Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -250,6 +250,60 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "oPromessa",
|
||||
"name": "oPromessa",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/21261491?v=4",
|
||||
"profile": "https://github.com/oPromessa",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "spencerc99",
|
||||
"name": "Spencer Chang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14796580?v=4",
|
||||
"profile": "http://spencerchang.me",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dgleich",
|
||||
"name": "David Gleich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
|
||||
"profile": "https://www.cs.purdue.edu/homes/dgleich",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alandefreitas",
|
||||
"name": "Alan de Freitas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
|
||||
"profile": "https://alandefreitas.github.io/alandefreitas/",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hyfen",
|
||||
"name": "Andrew Louis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
|
||||
"profile": "https://hyfen.net",
|
||||
"contributions": [
|
||||
"doc", "code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neebah",
|
||||
"name": "neebah",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
|
||||
"profile": "https://github.com/neebah",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -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
|
||||
docsrc/_build/
|
||||
venv/
|
||||
.python-version
|
||||
|
||||
204
CHANGELOG.md
@@ -4,6 +4,210 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.44.1](https://github.com/RhetTbull/osxphotos/compare/v0.44.0...v0.44.1)
|
||||
|
||||
> 31 December 2021
|
||||
|
||||
- Added --skip-uuid, --skip-uuid-from-file, #563 [`04930c3`](https://github.com/RhetTbull/osxphotos/commit/04930c3644da99c1923c4e3aaa9213902aeadfd1)
|
||||
|
||||
#### [v0.44.0](https://github.com/RhetTbull/osxphotos/compare/v0.43.9...v0.44.0)
|
||||
|
||||
> 31 December 2021
|
||||
|
||||
- Added support for projects, implements #559 [`44594a8`](https://github.com/RhetTbull/osxphotos/commit/44594a8e437c20bae6fd8eecb74075d49da4b91f)
|
||||
- Updated docs [skip ci] [`c4e3c5a`](https://github.com/RhetTbull/osxphotos/commit/c4e3c5a8beac1db00533f7820ab8249cf351aef0)
|
||||
- Fixed test for #561 [`690d981`](https://github.com/RhetTbull/osxphotos/commit/690d981f310b083f5f58407cc879bca494730765)
|
||||
|
||||
#### [v0.43.9](https://github.com/RhetTbull/osxphotos/compare/v0.43.8...v0.43.9)
|
||||
|
||||
> 28 December 2021
|
||||
|
||||
- Fix for accented characters in album names, #561 [`03f4e7c`](https://github.com/RhetTbull/osxphotos/commit/03f4e7cc3473c276dfd7c7e6ad64e4dfe5b32011)
|
||||
|
||||
#### [v0.43.8](https://github.com/RhetTbull/osxphotos/compare/v0.43.7...v0.43.8)
|
||||
|
||||
> 26 December 2021
|
||||
|
||||
- Fixed #463 [`#463`](https://github.com/RhetTbull/osxphotos/issues/463)
|
||||
- Updated docs [skip ci] [`181f678`](https://github.com/RhetTbull/osxphotos/commit/181f678d9eda8bc8acca11b4ebd470900f30bdcb)
|
||||
- Added install/uninstall commands, #531 [`085f482`](https://github.com/RhetTbull/osxphotos/commit/085f482820af2d51f0d411c7e8a7a27329bf0722)
|
||||
- Implement #323 [`debb17c`](https://github.com/RhetTbull/osxphotos/commit/debb17c9520bec25d725426feaa512745e9d4ec0)
|
||||
- Updated docs [skip ci] [`0e54a08`](https://github.com/RhetTbull/osxphotos/commit/0e54a08ae07853c4cdb2c548bdba27335cfc32ba)
|
||||
- Added get_photos_library_version [`b71c752`](https://github.com/RhetTbull/osxphotos/commit/b71c752e9d2c59412baf812bfc50e6358ea3f02e)
|
||||
|
||||
#### [v0.43.7](https://github.com/RhetTbull/osxphotos/compare/v0.43.6...v0.43.7)
|
||||
|
||||
> 21 December 2021
|
||||
|
||||
- Adds missing f-string to retry message [`#553`](https://github.com/RhetTbull/osxphotos/pull/553)
|
||||
- Update issue templates [`e7bd80e`](https://github.com/RhetTbull/osxphotos/commit/e7bd80e05f94238fd41e478e32c1709b442eb361)
|
||||
- Partial fix for #556 [`a08a653`](https://github.com/RhetTbull/osxphotos/commit/a08a653f202a49853780ab4a686bf3dfbc32a491)
|
||||
- Updated all-contributors [`e1f1772`](https://github.com/RhetTbull/osxphotos/commit/e1f1772080d24373ceb5791683615451cd390874)
|
||||
- Version bump [`6ce1b83`](https://github.com/RhetTbull/osxphotos/commit/6ce1b83ca2c7f0c6f9c86757602b81df1d9bf453)
|
||||
|
||||
#### [v0.43.6](https://github.com/RhetTbull/osxphotos/compare/v0.43.5...v0.43.6)
|
||||
|
||||
> 10 December 2021
|
||||
|
||||
- Fixes typo in README [`#548`](https://github.com/RhetTbull/osxphotos/pull/548)
|
||||
- docs: add alandefreitas as a contributor for bug [`#551`](https://github.com/RhetTbull/osxphotos/pull/551)
|
||||
- docs: add dgleich as a contributor for code [`#541`](https://github.com/RhetTbull/osxphotos/pull/541)
|
||||
- Updated docs [`197e566`](https://github.com/RhetTbull/osxphotos/commit/197e5663df058a013ce2d6f8c5fd7ff71a5cc46e)
|
||||
- Added test library for Monterey on M1 [`3e038bf`](https://github.com/RhetTbull/osxphotos/commit/3e038bf124b98d6b74f19dd4db0f8f1e3c48e787)
|
||||
- Updated docs [skip ci] [`f6dedaa`](https://github.com/RhetTbull/osxphotos/commit/f6dedaa6197dc244616c5b4e9e8ce42ce6b7a252)
|
||||
- Added MomentInfo for Photos 5+, #71 [`a52b4d2`](https://github.com/RhetTbull/osxphotos/commit/a52b4d2f43970086bf25659bd58dc8479b841704)
|
||||
- Fixed error for missing photo path, #547 [`0906dbe`](https://github.com/RhetTbull/osxphotos/commit/0906dbe6370922b4c9649350014ed8a21d29c4fd)
|
||||
|
||||
#### [v0.43.5](https://github.com/RhetTbull/osxphotos/compare/v0.43.4...v0.43.5)
|
||||
|
||||
> 25 November 2021
|
||||
|
||||
- Updated dependencies for pyobjc 8.0 [`7d92359`](https://github.com/RhetTbull/osxphotos/commit/7d923590ae4df941b1b9d35c21937c03eb7b4284)
|
||||
|
||||
#### [v0.43.4](https://github.com/RhetTbull/osxphotos/compare/v0.43.3...v0.43.4)
|
||||
|
||||
> 11 November 2021
|
||||
|
||||
- Fix for --use-photokit with --skip-live, #537 [`0e6c92d`](https://github.com/RhetTbull/osxphotos/commit/0e6c92dbd951dd0e63cfb8b6d64e6ab96ece5955)
|
||||
|
||||
#### [v0.43.3](https://github.com/RhetTbull/osxphotos/compare/v0.43.1...v0.43.3)
|
||||
|
||||
> 7 November 2021
|
||||
|
||||
- Updated docs [skip ci] [`fb583e2`](https://github.com/RhetTbull/osxphotos/commit/fb583e28e0fc2c23bf24052db8a5ee669d8c92f5)
|
||||
- Updated OTL to MTL [`2ffcf1e`](https://github.com/RhetTbull/osxphotos/commit/2ffcf1e82bfc013a4a9e0e7a709a7c1395c074ce)
|
||||
- Test fixes for Monterey/M1 [`51ba549`](https://github.com/RhetTbull/osxphotos/commit/51ba54971a874cfce00368aa5be5380b3439c254)
|
||||
|
||||
#### [v0.43.1](https://github.com/RhetTbull/osxphotos/compare/v0.43.0...v0.43.1)
|
||||
|
||||
> 30 October 2021
|
||||
|
||||
- Dependency update for Monterey [`818f4f4`](https://github.com/RhetTbull/osxphotos/commit/818f4f45a4ce520b0ba1c688eabd2f4311be9540)
|
||||
- Updated docs [skip ci] [`2cf19f6`](https://github.com/RhetTbull/osxphotos/commit/2cf19f6af1a03767e4d53eee556c4d3ed9af1776)
|
||||
|
||||
#### [v0.43.0](https://github.com/RhetTbull/osxphotos/compare/v0.42.94...v0.43.0)
|
||||
|
||||
> 28 October 2021
|
||||
|
||||
- Updated for Monterey 12.0.1 release [`ef82c6e`](https://github.com/RhetTbull/osxphotos/commit/ef82c6e32b536b0677530133892f95b852c6dce0)
|
||||
|
||||
#### [v0.42.94](https://github.com/RhetTbull/osxphotos/compare/v0.42.93...v0.42.94)
|
||||
|
||||
> 15 October 2021
|
||||
|
||||
- docs: add spencerc99 as a contributor for bug [`#527`](https://github.com/RhetTbull/osxphotos/pull/527)
|
||||
- Fix for #526 with --update [`419b34e`](https://github.com/RhetTbull/osxphotos/commit/419b34ea73f15ccbe29f51896e11e9735ea5786b)
|
||||
- Updated docs [skip ci] [`0e9b9d6`](https://github.com/RhetTbull/osxphotos/commit/0e9b9d625190b94c1dd68276e3b0e5367002d87c)
|
||||
- Fixed FileUtil to use correct import [`f64c4ed`](https://github.com/RhetTbull/osxphotos/commit/f64c4ed374c120a95fe8adea26bd44852ca67e31)
|
||||
|
||||
#### [v0.42.93](https://github.com/RhetTbull/osxphotos/compare/v0.42.92...v0.42.93)
|
||||
|
||||
> 11 October 2021
|
||||
|
||||
- Fix for #526 [`202bc11`](https://github.com/RhetTbull/osxphotos/commit/202bc1144bc842ddec825eef0745830d56170aba)
|
||||
- Updated README.md [skip ci] [`a0c654e`](https://github.com/RhetTbull/osxphotos/commit/a0c654e43f4aa5389a96c3c84fd7037c33d23404)
|
||||
|
||||
#### [v0.42.92](https://github.com/RhetTbull/osxphotos/compare/v0.42.91...v0.42.92)
|
||||
|
||||
> 11 October 2021
|
||||
|
||||
- docs: add oPromessa as a contributor for bug [`#525`](https://github.com/RhetTbull/osxphotos/pull/525)
|
||||
- Fix for #524 [`04ac0a1`](https://github.com/RhetTbull/osxphotos/commit/04ac0a11215b275178013e60c6a61b9f1b3603c9)
|
||||
- Fix for #525 [`d2b0bd4`](https://github.com/RhetTbull/osxphotos/commit/d2b0bd4e28cfdf3c930aa6ae3317549327b0e29c)
|
||||
- Updated docs [skip ci] [`2bb677d`](https://github.com/RhetTbull/osxphotos/commit/2bb677dc19abaf254bc66e2cd788676e0613e548)
|
||||
|
||||
#### [v0.42.91](https://github.com/RhetTbull/osxphotos/compare/v0.42.90...v0.42.91)
|
||||
|
||||
> 11 October 2021
|
||||
|
||||
- Updated docs [skip ci] [`b23e74f`](https://github.com/RhetTbull/osxphotos/commit/b23e74f8f5a8387564108c330c3f8ac11189860d)
|
||||
- Added python 3.10 to supported versions [`3f81a3c`](https://github.com/RhetTbull/osxphotos/commit/3f81a3c179dde37e9811ef19c847920bb3bd514c)
|
||||
- Updated dependencies [`a895833`](https://github.com/RhetTbull/osxphotos/commit/a895833c7f0a264488e671f1735f9e10d2618e2d)
|
||||
|
||||
#### [v0.42.90](https://github.com/RhetTbull/osxphotos/compare/v0.42.89...v0.42.90)
|
||||
|
||||
> 30 September 2021
|
||||
|
||||
- Updated REPL, now with more cowbell [`c472698`](https://github.com/RhetTbull/osxphotos/commit/c472698b1d0d8ff9f4d1bde715859bf766f99290)
|
||||
- Updated docs [skip ci] [`1ddb1de`](https://github.com/RhetTbull/osxphotos/commit/1ddb1de99841e65b690ffc1cbcc5e42e6e25f727)
|
||||
|
||||
#### [v0.42.89](https://github.com/RhetTbull/osxphotos/compare/v0.42.88...v0.42.89)
|
||||
|
||||
> 26 September 2021
|
||||
|
||||
- Updated docs [skip ci] [`bfbc156`](https://github.com/RhetTbull/osxphotos/commit/bfbc156821d2d262b7bd9c4437e23e310da10769)
|
||||
- Updated docs [skip ci] [`3abaa5a`](https://github.com/RhetTbull/osxphotos/commit/3abaa5ae84ca44cd900f1e3af4532ab405d41a09)
|
||||
- Fixed AlbumInfo.owner, #239 [`bfd6274`](https://github.com/RhetTbull/osxphotos/commit/bfd627460255c65f870bca6d036401e8792d29d5)
|
||||
|
||||
#### [v0.42.88](https://github.com/RhetTbull/osxphotos/compare/v0.42.87...v0.42.88)
|
||||
|
||||
> 26 September 2021
|
||||
|
||||
- Performance fix for #239, owner [`14710e3`](https://github.com/RhetTbull/osxphotos/commit/14710e31789d71b2c948a37722fb6054aca4d85e)
|
||||
- version bump [`06138e1`](https://github.com/RhetTbull/osxphotos/commit/06138e15d0b87e4865a9ef0cc542303edb44c861)
|
||||
|
||||
#### [v0.42.87](https://github.com/RhetTbull/osxphotos/compare/v0.42.86...v0.42.87)
|
||||
|
||||
> 26 September 2021
|
||||
|
||||
#### [v0.42.86](https://github.com/RhetTbull/osxphotos/compare/v0.42.85...v0.42.86)
|
||||
|
||||
> 26 September 2021
|
||||
|
||||
- Fix for #517, #239 [`ac47df8`](https://github.com/RhetTbull/osxphotos/commit/ac47df8475762fe8c8f63ad5ffa83b1e20d116b8)
|
||||
- Fixed formatting [`6adafb8`](https://github.com/RhetTbull/osxphotos/commit/6adafb8ce70e95a9f0bec1a3db6362742fcd1b0d)
|
||||
- Updated docs [skip ci] [`725f7c8`](https://github.com/RhetTbull/osxphotos/commit/725f7c87351353efeee8c43c3c7f8a95acb14490)
|
||||
|
||||
#### [v0.42.85](https://github.com/RhetTbull/osxphotos/compare/v0.42.84...v0.42.85)
|
||||
|
||||
> 25 September 2021
|
||||
|
||||
- Implemented PhotoInfo.owner, AlbumInfo.owner, #216, #239 [`c4b7c26`](https://github.com/RhetTbull/osxphotos/commit/c4b7c2623f077d9964d5d578ce6c01bb83fab088)
|
||||
- Updated docs [skip ci] [`59ba325`](https://github.com/RhetTbull/osxphotos/commit/59ba325273b2f16935be944fd46c1237ce637bb8)
|
||||
|
||||
#### [v0.42.84](https://github.com/RhetTbull/osxphotos/compare/v0.42.83...v0.42.84)
|
||||
|
||||
> 25 September 2021
|
||||
|
||||
- Fix for #516 [`e3e1da2`](https://github.com/RhetTbull/osxphotos/commit/e3e1da2fd898896595fc851288f905bd4e2150f8)
|
||||
- Updated docs [skip ci] [`64c226b`](https://github.com/RhetTbull/osxphotos/commit/64c226b85529581e393a2d0604b41c37a8dc2eaf)
|
||||
- Update docs [`c429a86`](https://github.com/RhetTbull/osxphotos/commit/c429a860b1ebeb77f3c3e36e9660fc9153d85d11)
|
||||
|
||||
#### [v0.42.83](https://github.com/RhetTbull/osxphotos/compare/v0.42.82...v0.42.83)
|
||||
|
||||
> 15 September 2021
|
||||
|
||||
- Fixed detected_text to use image orientation if available [`dd08c7f`](https://github.com/RhetTbull/osxphotos/commit/dd08c7f701335a7e1e30fda251e6ad20ff781652)
|
||||
- Added twine [`16335a6`](https://github.com/RhetTbull/osxphotos/commit/16335a6bd66eaa53fd1c390901e2fb028059d8e1)
|
||||
- Added wheel [`e0f6d8e`](https://github.com/RhetTbull/osxphotos/commit/e0f6d8ecf27fe772b748c7b2f3108558fbc23e8a)
|
||||
|
||||
#### [v0.42.82](https://github.com/RhetTbull/osxphotos/compare/v0.42.80...v0.42.82)
|
||||
|
||||
> 14 September 2021
|
||||
|
||||
- Fix for #515 [`93bf0c2`](https://github.com/RhetTbull/osxphotos/commit/93bf0c210cf01f351611427662025c86955ac373)
|
||||
- Fix for #515, updated tests [`59c31ff`](https://github.com/RhetTbull/osxphotos/commit/59c31ff88d099b251cf1b571279d7a28a0aac138)
|
||||
- Updated docs [`773dca8`](https://github.com/RhetTbull/osxphotos/commit/773dca849424c61a7447cb1bb87140708ab0a07c)
|
||||
|
||||
#### [v0.42.80](https://github.com/RhetTbull/osxphotos/compare/v0.42.79...v0.42.80)
|
||||
|
||||
> 29 August 2021
|
||||
|
||||
- Bug fix for null title, #512 [`6bcc676`](https://github.com/RhetTbull/osxphotos/commit/6bcc67634ca50e84494539b8a25eb7925dcede62)
|
||||
- Updated dependencies [`2eb6e70`](https://github.com/RhetTbull/osxphotos/commit/2eb6e70e57ff1dc79907a29618757953f5871145)
|
||||
- Updated README [skip ci] [`81dd1a7`](https://github.com/RhetTbull/osxphotos/commit/81dd1a753062dacc83aaf4ce8a7667de2cda599b)
|
||||
|
||||
#### [v0.42.79](https://github.com/RhetTbull/osxphotos/compare/v0.42.78...v0.42.79)
|
||||
|
||||
> 29 August 2021
|
||||
|
||||
#### [v0.42.78](https://github.com/RhetTbull/osxphotos/compare/v0.42.77...v0.42.78)
|
||||
|
||||
> 29 August 2021
|
||||
|
||||
- docs: add dssinger as a contributor for bug [`#514`](https://github.com/RhetTbull/osxphotos/pull/514)
|
||||
- Fix for newlines in exif tags, #513 [`f0d7496`](https://github.com/RhetTbull/osxphotos/commit/f0d7496bc66aae291337efc570a2e2c4b9b5529c)
|
||||
|
||||
#### [v0.42.77](https://github.com/RhetTbull/osxphotos/compare/v0.42.74...v0.42.77)
|
||||
|
||||
> 28 August 2021
|
||||
|
||||
@@ -2,4 +2,5 @@ include README.md
|
||||
include README.rst
|
||||
include osxphotos/templates/*
|
||||
include osxphotos/phototemplate.tx
|
||||
include osxphotos/phototemplate.md
|
||||
include osxphotos/phototemplate.md
|
||||
include osxphotos/queries/*
|
||||
167
README.md
@@ -4,7 +4,9 @@
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||

|
||||
[](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.
|
||||
|
||||
@@ -12,7 +14,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
# Table of Contents
|
||||
* [Supported operating systems](#supported-operating-systems)
|
||||
* [Installation instructions](#installation-instructions)
|
||||
* [Installation](#installation)
|
||||
* [Command Line Usage](#command-line-usage)
|
||||
+ [Command line examples](#command-line-examples)
|
||||
+ [Tutorial](#tutorial)
|
||||
@@ -23,6 +25,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
+ [ExifInfo](#exifinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [ImportInfo](#importinfo)
|
||||
+ [ProjectInfo](#projectinfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
@@ -50,13 +53,12 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
## 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 |
|
||||
| ----------------- |------------|:-------------------|
|
||||
| 12.0 | Monterey | ?.0 UNKNOWN |
|
||||
| 12.0 | Monterey | 7.0 ✅ |
|
||||
| 10.16, 11.0-11.4 | Big Sur | 6.0 ✅ |
|
||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||
@@ -138,20 +140,22 @@ Options:
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
dump Print list of all photos & associated info from the Photos...
|
||||
export Export photos from the Photos database.
|
||||
help Print help; for help on commands: help <command>.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options; if...
|
||||
repl Run interactive osxphotos shell
|
||||
tutorial Display osxphotos tutorial.
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
dump Print list of all photos & associated info from the Photos...
|
||||
export Export photos from the Photos database.
|
||||
help Print help; for help on commands: help <command>.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
install Install Python packages into the same environment as osxphotos
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options; if...
|
||||
repl Run interactive osxphotos REPL shell (useful for debugging,...
|
||||
tutorial Display osxphotos tutorial.
|
||||
uninstall Uninstall Python packages from the osxphotos environment
|
||||
```
|
||||
|
||||
To get help on a specific command, use `osxphotos help <command_name>`
|
||||
@@ -611,7 +615,8 @@ Options:
|
||||
FILENAME. If more than one --name options is
|
||||
specified, they are treated as "OR", e.g. find
|
||||
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
|
||||
FILE. Format is a single UUID per line. Lines
|
||||
preceded with # are ignored.
|
||||
@@ -728,6 +733,15 @@ Options:
|
||||
repeating '--regex' with different arguments.
|
||||
--selected Filter for photos that are currently selected
|
||||
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
|
||||
will be evaluated in context of the following
|
||||
python list comprehension: `photos = [photo
|
||||
@@ -824,6 +838,11 @@ Options:
|
||||
photos if the RAW photo does not have an
|
||||
associated JPEG image (e.g. the RAW file was
|
||||
imported to Photos without a JPEG preview).
|
||||
--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
|
||||
original filename for export. Note: Starting
|
||||
with Photos 5, all photos are renamed upon
|
||||
@@ -1258,8 +1277,8 @@ s
|
||||
** Templating System **
|
||||
|
||||
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.
|
||||
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.
|
||||
@@ -1702,7 +1721,7 @@ Substitution Description
|
||||
{lf} A line feed: '\n', alias for {newline}
|
||||
{cr} A carriage return: '\r'
|
||||
{crlf} a carriage return + line feed: '\r\n'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.42.78'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.44.1'
|
||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified for
|
||||
@@ -1717,6 +1736,13 @@ Substitution Description
|
||||
{folder_album} Folder path + album photo is contained in. e.g.
|
||||
'Folder/Subfolder/Album' or just 'Album' if no
|
||||
enclosing folder
|
||||
{project} Project(s) photo is contained in (such as greeting
|
||||
cards, calendars, slideshows)
|
||||
{album_project} Album(s) and project(s) photo is contained in; treats
|
||||
projects as regular albums
|
||||
{folder_album_project} Folder path + album (includes projects as albums)
|
||||
photo is contained in. e.g. 'Folder/Subfolder/Album'
|
||||
or just 'Album' if no enclosing folder
|
||||
{keyword} Keyword(s) assigned to photo
|
||||
{person} Person(s) / face(s) in a photo
|
||||
{label} Image categorization label associated with a photo
|
||||
@@ -1820,7 +1846,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
|
||||
following categories are available:
|
||||
|
||||
Catgory Description
|
||||
Category Description
|
||||
exported All exported files
|
||||
new When used with '--update', all newly exported files
|
||||
updated When used with '--update', all files which were
|
||||
@@ -1861,13 +1887,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
|
||||
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
|
||||
'{filepath.name}' 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 thus renders to:
|
||||
In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is
|
||||
properly quoted and the '{shell_quote}' template ensures the constructed path of
|
||||
'{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is 'IMG
|
||||
1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus
|
||||
renders to:
|
||||
|
||||
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
|
||||
|
||||
@@ -2091,7 +2117,7 @@ keywords = photosdb.keywords
|
||||
|
||||
Returns a list of the keywords found in the Photos library
|
||||
|
||||
#### `album_info`
|
||||
#### <a name="photosdbalbuminfo">`album_info`</a>
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
albums = photosdb.album_info
|
||||
@@ -2121,6 +2147,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.
|
||||
|
||||
#### `project_info`
|
||||
|
||||
Returns a list of [ProjectInfo](#projectinfo) objects representing the projects/creations (cards, calendars, etc.) in the database.
|
||||
|
||||
#### `folder_info`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -2373,6 +2403,8 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
|
||||
#### <a name="getphoto">`get_photo(uuid)`</A>
|
||||
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
||||
|
||||
#### `execute(sql)`
|
||||
Execute sql statement against the Photos database and return a sqlite cursor with the results.
|
||||
|
||||
### PhotoInfo
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
||||
@@ -2408,11 +2440,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).
|
||||
|
||||
#### `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`
|
||||
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`
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
@@ -2516,7 +2552,12 @@ Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None i
|
||||
#### `shared`
|
||||
Returns True if photo is in a shared album, otherwise False.
|
||||
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None instead of True/False.
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||
|
||||
#### `owner`
|
||||
Returns full name of the photo owner (person who shared the photo) for shared photos or None if photo is not shared. Also returns None if you are the person who shared the photo.
|
||||
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||
|
||||
#### `comments`
|
||||
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
|
||||
@@ -2766,7 +2807,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.
|
||||
|
||||
- `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.
|
||||
|
||||
`RenderOptions` has the following properties:
|
||||
@@ -2890,6 +2931,11 @@ Photos Library
|
||||
#### `parent`
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||
|
||||
#### `owner`
|
||||
Returns full name of the album owner (person who shared the album) for shared albums or None if album is not shared.
|
||||
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||
|
||||
### ImportInfo
|
||||
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
||||
|
||||
@@ -2910,6 +2956,23 @@ Returns the start date as a timezone aware datetime.datetime object for when the
|
||||
#### `end_date`
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -3257,7 +3320,6 @@ The following additional properties are also available but are not yet fully doc
|
||||
- `manual`:
|
||||
- `face_type`:
|
||||
- `age_type`:
|
||||
- `bald_type`:
|
||||
- `eye_makeup_type`:
|
||||
- `eye_state`:
|
||||
- `facial_hair_type`:
|
||||
@@ -3340,7 +3402,7 @@ To get the path of every raw photo, whether it's a single raw photo or a raw+JPE
|
||||
### Template System
|
||||
|
||||
<!-- 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.
|
||||
|
||||
@@ -3561,10 +3623,13 @@ The following template field substitutions are availabe for use the templating s
|
||||
|{lf}|A line feed: '\n', alias for {newline}|
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.42.78'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.44.1'|
|
||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||
|{album}|Album(s) photo is contained in|
|
||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||
|{project}|Project(s) photo is contained in (such as greeting cards, calendars, slideshows)|
|
||||
|{album_project}|Album(s) and project(s) photo is contained in; treats projects as regular albums|
|
||||
|{folder_album_project}|Folder path + album (includes projects as albums) photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||
|{keyword}|Keyword(s) assigned to photo|
|
||||
|{person}|Person(s) / face(s) in a photo|
|
||||
|{label}|Image categorization label associated with a photo (Photos 5+ only). 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.|
|
||||
@@ -3666,13 +3731,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.
|
||||
|
||||
#### `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
|
||||
|
||||
@@ -3728,15 +3786,10 @@ if __name__ == "__main__":
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [rhettbull/photosmeta](https://github.com/rhettbull/photosmeta): uses osxphotos and [exiftool](https://exiftool.org/) to apply metadata from Photos as exif data in the photo files. Can also export photos while preserving metadata and also apply Photos keywords as spotlight tags to make it easier to search for photos using spotlight. This is mostly made obsolete by osxphotos. The one feature that photosmeta has that osxphotos does not is ability to update the metadata of the actual photo files in the Photos library without exporting them. (Use with caution!)
|
||||
- [rhettbull/exif2findertags](https://github.com/RhetTbull/exif2findertags): Read EXIF metadata from image and video files and convert it to macOS Finder tags and/or Finder comments and other extended attributes.
|
||||
- [rhettbull/photos_time_warp](https://github.com/RhetTbull/photos_time_warp): Batch adjust the date, time, or timezone of photos in Apple Photos.
|
||||
- [rhettbull/PhotoScript](https://github.com/RhetTbull/PhotoScript): python wrapper around Photos' applescript API allowing automation of Photos (including creation/deletion of items) from python.
|
||||
- [patrikhson/photo-export](https://github.com/patrikhson/photo-export): Exports older versions of Photos databases. Provided the inspiration for osxphotos.
|
||||
- [doersino/apple-photos-export](https://github.com/doersino/apple-photos-export): Photos export script for Mojave.
|
||||
- [orangeturtle739/photos-export](https://github.com/orangeturtle739/photos-export): Set of scripts to export Photos libraries.
|
||||
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud. Currently unmaintained.
|
||||
- [AaronVanGeffen/ExportPhotosLibrary](https://github.com/AaronVanGeffen/ExportPhotosLibrary): Another python script for exporting older versions of Photos libraries.
|
||||
- [MossieurPropre/PhotosAlbumExporter](https://github.com/MossieurPropre/PhotosAlbumExporter): Javascript script to export photos while maintaining album structure.
|
||||
- [ajslater/magritte](https://github.com/ajslater/magritte): Another python command line script for exporting photos from older versions of Photos libraries.
|
||||
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -3790,6 +3843,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||
<td align="center"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt=""/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||
<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></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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -3831,6 +3892,8 @@ For additional details about how osxphotos is implemented or if you would like t
|
||||
- [textx](https://github.com/textX/textX)
|
||||
- [bitmath](https://github.com/tbielawa/bitmath)
|
||||
- [more-itertools](https://github.com/more-itertools/more-itertools)
|
||||
- [ptpython](https://github.com/prompt-toolkit/ptpython)
|
||||
- [objexplore](https://github.com/kylepollina/objexplore)
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
7
build.sh
@@ -3,9 +3,10 @@
|
||||
# script to help build osxphotos release
|
||||
# this is unique to my own dev setup
|
||||
|
||||
source venv/bin/activate
|
||||
# source venv/bin/activate
|
||||
rm -rf dist; rm -rf build
|
||||
python3 utils/update_readme.py
|
||||
(cd docsrc && make github && make pdf)
|
||||
python3 setup.py sdist bdist_wheel
|
||||
./make_cli_exe.sh
|
||||
# python3 setup.py sdist bdist_wheel
|
||||
python3 -m build
|
||||
./make_cli_exe.sh
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
build
|
||||
m2r2
|
||||
pyinstaller==4.4
|
||||
pytest-mock
|
||||
pytest==6.2.4
|
||||
pytest-mock==3.6.1
|
||||
Sphinx==4.0.2
|
||||
sphinx-rtd-theme==0.5.2
|
||||
wheel==0.36.2
|
||||
twine==3.4.1
|
||||
pyinstaller==4.3
|
||||
sphinx_click
|
||||
sphinx_rtd_theme
|
||||
twine
|
||||
wheel
|
||||
Sphinx
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 23e7c9cd300c96ffa7fce04034b83f61
|
||||
config: 58505ca56d322ccc59c38bc440ccc347
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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/alabaster.css" />
|
||||
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||
@@ -71,7 +71,7 @@
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
©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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_export — 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/alabaster.css" />
|
||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -89,7 +89,7 @@
|
||||
<span class="p">)</span>
|
||||
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
|
||||
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
|
||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">findfiles</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">noop</span>
|
||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">increment_filename_with_count</span><span class="p">,</span> <span class="n">lineno</span>
|
||||
|
||||
<span class="c1"># retry if use_photos_export fails the first time (which sometimes it does)</span>
|
||||
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
|
||||
@@ -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="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"># 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_suffix</span><span class="o">=</span><span class="n">DEFAULT_PREVIEW_SUFFIX</span><span class="p">,</span>
|
||||
<span class="n">render_options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""export photo, like export but with update and dry_run options</span>
|
||||
<span class="sd"> dest: must be valid destination path or exception raised</span>
|
||||
@@ -621,6 +622,7 @@
|
||||
<span class="sd"> preview: if True, also exports preview image</span>
|
||||
<span class="sd"> preview_suffix: optional string to append to end of filename for preview images</span>
|
||||
<span class="sd"> render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</span>
|
||||
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
|
||||
|
||||
<span class="sd"> Returns: ExportResults class</span>
|
||||
<span class="sd"> ExportResults has attributes:</span>
|
||||
@@ -714,15 +716,12 @@
|
||||
<span class="c1"># e.g. exporting sidecar for file1.png and file1.jpeg</span>
|
||||
<span class="c1"># if file1.png exists and exporting file1.jpeg,</span>
|
||||
<span class="c1"># dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision</span>
|
||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</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_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span>
|
||||
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
||||
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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="n">increment_file_count</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">dest_original</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
|
||||
<span class="n">dest_original</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_original</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
||||
<span class="k">if</span> <span class="p">(</span>
|
||||
@@ -737,17 +736,11 @@
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">export_edited</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</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_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span>
|
||||
<span class="k">if</span> <span class="n">count</span><span class="p">:</span>
|
||||
<span class="c1"># incremented above when checking original destination</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
|
||||
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_edited</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
||||
<span class="k">if</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">increment</span><span class="p">:</span>
|
||||
@@ -831,20 +824,16 @@
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
||||
<span class="c1"># not the right file, find the right one</span>
|
||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
|
||||
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</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">found_match</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
||||
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
||||
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="k">break</span>
|
||||
<span class="k">elif</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">cmp</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">file_</span><span class="p">):</span>
|
||||
<span class="c1"># files match, update the UUID</span>
|
||||
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
||||
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="p">,</span>
|
||||
<span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
@@ -856,18 +845,14 @@
|
||||
<span class="n">exif_json</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">found_match</span><span class="p">:</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># increment the destination file</span>
|
||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
|
||||
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</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="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">stem</span>
|
||||
<span class="k">while</span> <span class="n">dest_new</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">export_original</span><span class="p">:</span>
|
||||
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest</span>
|
||||
|
||||
<span class="c1"># export the dest file</span>
|
||||
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
||||
@@ -960,6 +945,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_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="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="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
||||
<span class="n">preview_path</span><span class="p">,</span>
|
||||
@@ -1002,6 +995,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||
<span class="p">(</span>
|
||||
@@ -1028,6 +1022,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||
<span class="p">(</span>
|
||||
@@ -1050,6 +1045,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||
<span class="p">(</span>
|
||||
@@ -1120,6 +1116,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
|
||||
@@ -1143,6 +1140,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
||||
@@ -1163,6 +1161,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">),</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
||||
@@ -1188,6 +1187,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
||||
@@ -1208,6 +1208,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">),</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
||||
@@ -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">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">video</span><span class="o">=</span><span class="n">live_photo</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="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">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">video</span><span class="o">=</span><span class="n">live_photo</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="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="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="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="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="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">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="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">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">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""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"> 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"> strip: if True, strip any leading or trailing whitespace from rendered templates</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
|
||||
@@ -1643,6 +1663,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
|
||||
@@ -1668,6 +1689,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
||||
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
||||
@@ -1684,6 +1706,7 @@
|
||||
<span class="sd"> persons: if True, include person data</span>
|
||||
<span class="sd"> location: if True, include location data</span>
|
||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||
<span class="sd"> strip: if True, strip any rendered templates</span>
|
||||
|
||||
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
||||
|
||||
@@ -1731,6 +1754,8 @@
|
||||
<span class="p">)</span>
|
||||
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="n">description</span> <span class="o">=</span> <span class="s2">" "</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">"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>
|
||||
@@ -1778,6 +1803,9 @@
|
||||
<span class="p">)</span>
|
||||
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="n">keyword</span>
|
||||
@@ -1884,12 +1912,6 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
|
||||
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%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>
|
||||
|
||||
|
||||
@@ -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">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
||||
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
||||
@@ -1959,6 +1982,7 @@
|
||||
<span class="sd"> persons: if True, include person data</span>
|
||||
<span class="sd"> location: if True, include location data</span>
|
||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
|
||||
|
||||
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
||||
|
||||
@@ -1998,6 +2022,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
|
||||
@@ -2023,6 +2048,7 @@
|
||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""returns string for XMP sidecar</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"> 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"> strip: if True, strip whitespace from rendered templates</span>
|
||||
<span class="sd"> """</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="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="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">description</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
@@ -2093,6 +2122,9 @@
|
||||
<span class="p">)</span>
|
||||
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="n">keyword</span>
|
||||
@@ -2180,7 +2212,7 @@
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -2202,7 +2234,7 @@
|
||||
©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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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/alabaster.css" />
|
||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -47,6 +47,7 @@
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">yaml</span>
|
||||
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
|
||||
@@ -67,9 +68,11 @@
|
||||
<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">..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">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
|
||||
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
|
||||
<span class="kn">from</span> <span class="nn">..query_builder</span> <span class="kn">import</span> <span class="n">get_query</span>
|
||||
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
|
||||
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
|
||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
|
||||
@@ -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="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="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>
|
||||
@@ -596,7 +611,12 @@
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""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="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">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="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">photopath</span> <span class="o">=</span> <span class="kc">None</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"># or could do the actual check with "isfile"</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="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># Photos 5</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">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="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">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="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"># In testing, I've seen occasional missing movie for live photo</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="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="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="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">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="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"># In testing, I've seen occasional missing movie for live photo</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">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>
|
||||
|
||||
@@ -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="c1"># For Photos 5+, try to get the adjusted orientation</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># can't reliably determine orientation for edited photo if adjustmentinfo not available</span>
|
||||
<span class="k">return</span> <span class="mi">0</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"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="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>
|
||||
@@ -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="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>
|
||||
<span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="p">):</span>
|
||||
@@ -1151,6 +1183,28 @@
|
||||
|
||||
<span class="sd"> Returns: list of (detected text, confidence) tuples</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="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1158,23 +1212,13 @@
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span>
|
||||
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
|
||||
<span class="k">if</span> <span class="n">confidence</span> <span class="o">>=</span> <span class="n">confidence_threshold</span>
|
||||
<span class="p">]</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span></div>
|
||||
<span class="n">md</span> <span class="o">=</span> <span class="n">OSXMetaData</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">get_attribute</span><span class="p">(</span><span class="s2">"osxphotos_detected_text"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">detected_text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span> <span class="ow">or</span> <span class="kc">None</span>
|
||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">orientation</span><span class="p">)</span>
|
||||
<span class="n">md</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">"osxphotos_detected_text"</span><span class="p">,</span> <span class="n">detected_text</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">detected_text</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
@@ -1433,7 +1477,7 @@
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -1455,7 +1499,7 @@
|
||||
©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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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/alabaster.css" />
|
||||
<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">tempfile</span>
|
||||
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
|
||||
<span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
|
||||
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||||
<span class="kn">from</span> <span class="nn">pprint</span> <span class="kn">import</span> <span class="n">pformat</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">bitmath</span>
|
||||
<span class="kn">import</span> <span class="nn">photoscript</span>
|
||||
<span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
|
||||
@@ -283,6 +285,10 @@
|
||||
<span class="c1"># Dict to hold information on volume names (Photos 5+)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_filesystem_volumes</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="c1"># Dict to hold information on moments (Photos 5+)</span>
|
||||
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"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="bp">self</span><span class="o">.</span><span class="n">_process_database5</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">keywords_as_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""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">"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">"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">"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="p">}</span>
|
||||
|
||||
<span class="c1"># get details about folders</span>
|
||||
@@ -1137,7 +1145,9 @@
|
||||
<span class="c1"># get info on special types</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"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">"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">"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>
|
||||
@@ -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">"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="n">signature</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_duplicate_signature</span><span class="p">(</span><span class="n">uuid</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
@@ -1956,7 +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">.ZSAVEDASSETTYPE,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZADDEDDATE,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZCLOUDOWNERHASHEDPERSONID</span>
|
||||
<span class="s2"> FROM </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2"> </span>
|
||||
<span class="s2"> JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK </span>
|
||||
<span class="s2"> ORDER BY </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID """</span>
|
||||
@@ -2006,6 +2020,7 @@
|
||||
<span class="c1"># 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported</span>
|
||||
<span class="c1"># 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library</span>
|
||||
<span class="c1"># 42 ZGENERICASSET.Z_PK -- primary key</span>
|
||||
<span class="c1"># 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -2191,6 +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">"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"># 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="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="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>
|
||||
@@ -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="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="sd">"""recursively build folder/album hierarchy</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="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">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">regex</span><span class="p">,</span> <span class="n">template</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">regex</span><span class="p">:</span>
|
||||
<span class="n">regex</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">regex</span><span class="p">,</span> <span class="n">flags</span><span class="p">)</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="n">rendered</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">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>
|
||||
@@ -3381,12 +3504,45 @@
|
||||
<span class="c1"># selection only works if photos selected in main media browser</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
|
||||
<span class="n">matching_photos</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="p">:</span>
|
||||
<span class="k">continue</span>
|
||||
<span class="n">exifdata</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="n">exifdata</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">tag_groups</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
|
||||
<span class="k">for</span> <span class="n">exiftag</span><span class="p">,</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="n">exifvalue</span> <span class="o">=</span> <span class="n">exifvalue</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">exiftag</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="s2">""</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">for</span> <span class="n">function</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">function</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">photos</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photos</span></div>
|
||||
|
||||
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span>
|
||||
<span class="sd">"""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="sd">"""Compute a signature for finding possible duplicates"""</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"> Includes recently deleted photos and non-selected burst images</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>
|
||||
@@ -3477,7 +3637,7 @@
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -3499,7 +3659,7 @@
|
||||
©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>
|
||||
|
||||
</div>
|
||||
|
||||
5
docs/_static/basic.css
vendored
@@ -731,8 +731,9 @@ dl.glossary dt {
|
||||
|
||||
.classifier:before {
|
||||
font-style: normal;
|
||||
margin: 0.5em;
|
||||
margin: 0 0.5em;
|
||||
content: ":";
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
abbr, acronym {
|
||||
@@ -819,7 +820,7 @@ div.code-block-caption code {
|
||||
|
||||
table.highlighttable td.linenos,
|
||||
span.linenos,
|
||||
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
|
||||
div.highlight span.gp { /* gp: Generic.Prompt */
|
||||
user-select: none;
|
||||
-webkit-user-select: text; /* Safari fallback only */
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
|
||||
2
docs/_static/doctools.js
vendored
@@ -301,12 +301,14 @@ var Documentation = {
|
||||
window.location.href = prevHref;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 39: // right
|
||||
var nextHref = $('link[rel="next"]').prop('href');
|
||||
if (nextHref) {
|
||||
window.location.href = nextHref;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.42.69',
|
||||
VERSION: '0.44.1',
|
||||
LANGUAGE: 'None',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
13
docs/_static/searchtools.js
vendored
@@ -282,7 +282,10 @@ var Search = {
|
||||
complete: function(jqxhr, textstatus) {
|
||||
var data = jqxhr.responseText;
|
||||
if (data !== '' && data !== undefined) {
|
||||
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
|
||||
var summary = Search.makeSearchSummary(data, searchterms, hlterms);
|
||||
if (summary) {
|
||||
listItem.append(summary);
|
||||
}
|
||||
}
|
||||
Search.output.append(listItem);
|
||||
setTimeout(function() {
|
||||
@@ -325,7 +328,9 @@ var Search = {
|
||||
var results = [];
|
||||
|
||||
for (var prefix in objects) {
|
||||
for (var name in objects[prefix]) {
|
||||
for (var iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
|
||||
var match = objects[prefix][iMatch];
|
||||
var name = match[4];
|
||||
var fullname = (prefix ? prefix + '.' : '') + name;
|
||||
var fullnameLower = fullname.toLowerCase()
|
||||
if (fullnameLower.indexOf(object) > -1) {
|
||||
@@ -339,7 +344,6 @@ var Search = {
|
||||
} else if (parts[parts.length - 1].indexOf(object) > -1) {
|
||||
score += Scorer.objPartialMatch;
|
||||
}
|
||||
var match = objects[prefix][name];
|
||||
var objname = objnames[match[1]][2];
|
||||
var title = titles[match[0]];
|
||||
// If more than one term searched for, we require other words to be
|
||||
@@ -498,6 +502,9 @@ var Search = {
|
||||
*/
|
||||
makeSearchSummary : function(htmlText, keywords, hlwords) {
|
||||
var text = Search.htmlToText(htmlText);
|
||||
if (text == "") {
|
||||
return null;
|
||||
}
|
||||
var textLower = text.toLowerCase();
|
||||
var start = 0;
|
||||
$.each(keywords, function() {
|
||||
|
||||
1588
docs/cli.html
2331
docs/genindex.html
106
docs/index.html
@@ -4,8 +4,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.42.69 documentation</title>
|
||||
<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.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
@@ -31,30 +32,30 @@
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="welcome-to-osxphotos-s-documentation">
|
||||
<section id="welcome-to-osxphotos-s-documentation">
|
||||
<h1>Welcome to osxphotos’s documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline">¶</a></h1>
|
||||
</div>
|
||||
<div class="section" id="osxphotos">
|
||||
</section>
|
||||
<section id="osxphotos">
|
||||
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="section" id="what-is-osxphotos">
|
||||
<section id="what-is-osxphotos">
|
||||
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline">¶</a></h2>
|
||||
<p>OSXPhotos provides both the ability to interact with and query 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.
|
||||
You can query the Photos library database – for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
|
||||
You can also easily export both the original and edited photos.</p>
|
||||
</div>
|
||||
<div class="section" id="supported-operating-systems">
|
||||
</section>
|
||||
<section id="supported-operating-systems">
|
||||
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).</p>
|
||||
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
|
||||
<p>This package will read Photos databases for any supported version on any supported macOS version.
|
||||
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
|
||||
<p>Requires python >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
</section>
|
||||
<section id="installation">
|
||||
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline">¶</a></h2>
|
||||
<p>If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.</p>
|
||||
<div class="section" id="installation-using-pipx">
|
||||
<section id="installation-using-pipx">
|
||||
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline">¶</a></h3>
|
||||
<p>If you 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">
|
||||
@@ -64,15 +65,15 @@ E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine
|
||||
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
|
||||
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation-using-pip">
|
||||
</section>
|
||||
<section id="installation-using-pip">
|
||||
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline">¶</a></h3>
|
||||
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="installation-from-git-repository">
|
||||
</section>
|
||||
<section id="installation-from-git-repository">
|
||||
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline">¶</a></h3>
|
||||
<p>OSXPhotos uses setuptools, thus simply run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
|
||||
@@ -87,9 +88,9 @@ I recommend you install the latest version from <a class="reference external" hr
|
||||
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
|
||||
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
|
||||
If you 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>
|
||||
</div>
|
||||
<div class="section" id="command-line-usage">
|
||||
</section>
|
||||
</section>
|
||||
<section id="command-line-usage">
|
||||
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h2>
|
||||
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
|
||||
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
|
||||
@@ -127,38 +128,38 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre"><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>
|
||||
<div class="section" id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
|
||||
<section id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
|
||||
<h4>export all photos to ~/Desktop/export group in folders by date created<a class="headerlink" href="#export-all-photos-to-desktop-export-group-in-folders-by-date-created" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
<p><strong>Note</strong>: Photos library/database path can also be specified using <code class="docutils literal notranslate"><span class="pre">--db</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">--db</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
</section>
|
||||
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">>results.json</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
<h4>export photos to file structure based on 4-digit year and full name of month of 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>(by default, it will attempt to use the system library)</p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
<h4>export photos to file structure based on 4-digit year of 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>
|
||||
</div>
|
||||
<div class="section" id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
</section>
|
||||
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
<h4>export default library using ‘country name/year’ as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{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>
|
||||
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
</section>
|
||||
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">"Big</span> <span class="pre">Videos"</span></code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="example-uses-of-the-package">
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section id="example-uses-of-the-package">
|
||||
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Simple usage of the package """</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="package-interface">
|
||||
</section>
|
||||
<section id="package-interface">
|
||||
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
|
||||
<div class="toctree-wrapper compound">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-persons">persons</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
|
||||
<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="cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="indices-and-tables">
|
||||
</section>
|
||||
</section>
|
||||
<section id="indices-and-tables">
|
||||
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -351,7 +333,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -373,7 +355,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
©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>
|
||||
|
||||
|
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos — osxphotos 0.42.69 documentation</title>
|
||||
<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.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
@@ -30,11 +31,11 @@
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="osxphotos">
|
||||
<section id="osxphotos">
|
||||
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="toctree-wrapper compound">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -69,7 +70,7 @@
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -91,7 +92,7 @@
|
||||
©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>
|
||||
|
||||
|
|
||||
|
||||
BIN
docs/objects.inv
1338
docs/reference.html
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.42.69 documentation</title>
|
||||
<title>Search — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
|
||||
@@ -38,13 +38,14 @@
|
||||
|
||||
<h1 id="search-documentation">Search</h1>
|
||||
|
||||
<div id="fallback" class="admonition warning">
|
||||
<script>$('#fallback').hide();</script>
|
||||
<noscript>
|
||||
<div class="admonition warning">
|
||||
<p>
|
||||
Please activate JavaScript to enable the search
|
||||
functionality.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
|
||||
<p>
|
||||
@@ -54,7 +55,7 @@
|
||||
|
||||
|
||||
<form action="" method="get">
|
||||
<input type="text" name="q" aria-labelledby="search-documentation" value="" />
|
||||
<input type="text" name="q" aria-labelledby="search-documentation" value="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="search" />
|
||||
<span id="search-progress" style="padding-left: 10px"></span>
|
||||
</form>
|
||||
@@ -110,7 +111,7 @@
|
||||
©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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
||||
# Photos 6 (10.16.0 Beta) == 14104
|
||||
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
|
||||
|
||||
_PHOTOS_2_VERSION = "2622"
|
||||
|
||||
# only version 3 - 4 have RKVersion.selfPortrait
|
||||
_PHOTOS_3_VERSION = "3301"
|
||||
|
||||
@@ -94,6 +96,8 @@ _TESTED_OS_VERSIONS = [
|
||||
("11", "2"),
|
||||
("11", "3"),
|
||||
("11", "4"),
|
||||
("11", "5"),
|
||||
("11", "6"),
|
||||
]
|
||||
|
||||
# Photos 5 has persons who are empty string if unidentified face
|
||||
@@ -119,12 +123,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||
# Constants used for processing folders and albums
|
||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND = 1508 # My Projects (e.g. Calendar, Card, Slideshow)
|
||||
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
||||
|
||||
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
||||
_PHOTOS_4_ALBUM_TYPE_ALBUM = 1 # RKAlbum.albumType
|
||||
_PHOTOS_4_ALBUM_TYPE_PROJECT = 9 # RKAlbum.albumType
|
||||
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW = 8 # RKAlbum.albumType
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS = [
|
||||
"TopLevelAlbums",
|
||||
"TopLevelKeepsakes",
|
||||
"TopLevelSlideshows",
|
||||
]
|
||||
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
|
||||
|
||||
# EXIF related constants
|
||||
@@ -275,12 +287,15 @@ POST_COMMAND_CATEGORIES = {
|
||||
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
||||
}
|
||||
|
||||
|
||||
class AlbumSortOrder(Enum):
|
||||
"""Album Sort Order"""
|
||||
|
||||
UNKNOWN = 0
|
||||
MANUAL = 1
|
||||
NEWEST_FIRST = 2
|
||||
OLDEST_FIRST = 3
|
||||
TITLE = 5
|
||||
|
||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
||||
|
||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.42.78"
|
||||
__version__ = "0.44.2"
|
||||
|
||||
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from ._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
@@ -22,6 +22,7 @@ from ._constants import (
|
||||
AlbumSortOrder,
|
||||
)
|
||||
from .datetime_utils import get_local_tz
|
||||
from .query_builder import get_query
|
||||
|
||||
|
||||
def sort_list_by_keys(values, sort_keys):
|
||||
@@ -131,6 +132,28 @@ class AlbumInfoBaseClass:
|
||||
def photos(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._owner
|
||||
except AttributeError:
|
||||
try:
|
||||
personid = self._db._dbalbum_details[self.uuid][
|
||||
"cloudownerhashedpersonid"
|
||||
]
|
||||
self._owner = (
|
||||
self._db._db_hashed_person_id[personid]["full_name"]
|
||||
if personid
|
||||
else None
|
||||
)
|
||||
except KeyError:
|
||||
self._owner = None
|
||||
return self._owner
|
||||
|
||||
def __len__(self):
|
||||
"""return number of photos contained in album"""
|
||||
return len(self.photos)
|
||||
@@ -138,7 +161,6 @@ class AlbumInfoBaseClass:
|
||||
|
||||
class AlbumInfo(AlbumInfoBaseClass):
|
||||
"""
|
||||
Base class for AlbumInfo, ImportInfo
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
@@ -208,7 +230,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
else None
|
||||
)
|
||||
else:
|
||||
@@ -243,18 +265,17 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
|
||||
def photo_index(self, photo):
|
||||
"""return index of photo in album (based on album sort order)"""
|
||||
index = 0
|
||||
for p in self.photos:
|
||||
for index, p in enumerate(self.photos):
|
||||
if p.uuid == photo.uuid:
|
||||
return index
|
||||
index += 1
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Photo with uuid {photo.uuid} does not appear to be in this album"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Photo with uuid {photo.uuid} does not appear to be in this album"
|
||||
)
|
||||
|
||||
|
||||
class ImportInfo(AlbumInfoBaseClass):
|
||||
"""Information about import sessions"""
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
"""return list of photos contained in import session"""
|
||||
@@ -273,6 +294,15 @@ class ImportInfo(AlbumInfoBaseClass):
|
||||
return self._photos
|
||||
|
||||
|
||||
class ProjectInfo(AlbumInfo):
|
||||
"""
|
||||
ProjectInfo with info about projects
|
||||
Projects are cards, calendars, slideshows, etc.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class FolderInfo:
|
||||
"""
|
||||
Info about a specific folder, contains all the details about the folder
|
||||
@@ -334,7 +364,7 @@ class FolderInfo:
|
||||
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
else None
|
||||
)
|
||||
else:
|
||||
|
||||
202
osxphotos/cli.py
@@ -20,6 +20,7 @@ import photoscript
|
||||
import rich.traceback
|
||||
import yaml
|
||||
from rich import pretty
|
||||
from runpy import run_module
|
||||
|
||||
import osxphotos
|
||||
|
||||
@@ -55,13 +56,14 @@ from .exiftool import get_exiftool_path
|
||||
from .export_db import ExportDB, ExportDBInMemory
|
||||
from .fileutil import FileUtil, FileUtilNoOp
|
||||
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||
from .photoinfo import ExportResults
|
||||
from .photoinfo import ExportResults, PhotoInfo
|
||||
from .photokit import check_photokit_authorization, request_photokit_authorization
|
||||
from .photosalbum import PhotosAlbum
|
||||
from .phototemplate import PhotoTemplate, RenderOptions
|
||||
from .pyrepl import embed_repl
|
||||
from .queryoptions import QueryOptions
|
||||
from .uti import get_preferred_uti_extension
|
||||
from .utils import expand_and_validate_filepath, load_function
|
||||
from .utils import expand_and_validate_filepath, load_function, normalize_fs_path
|
||||
|
||||
# global variable to control verbose output
|
||||
# set via --verbose/-V
|
||||
@@ -296,7 +298,8 @@ def QUERY_OPTIONS(f):
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s).",
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
o(
|
||||
"--uuid-from-file",
|
||||
@@ -541,6 +544,17 @@ def QUERY_OPTIONS(f):
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
o(
|
||||
"--exif",
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
o(
|
||||
"--query-eval",
|
||||
metavar="CRITERIA",
|
||||
@@ -686,6 +700,23 @@ def cli(ctx, db, json_, debug):
|
||||
"Note: this does not skip RAW photos if the RAW photo does not have an associated JPEG image "
|
||||
"(e.g. the RAW file was imported to Photos without a JPEG preview).",
|
||||
)
|
||||
@click.option(
|
||||
"--skip-uuid",
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Skip photos with UUID(s) during export. "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
)
|
||||
@click.option(
|
||||
"--skip-uuid-from-file",
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Skip photos with UUID(s) loaded from FILE. "
|
||||
"Format is a single UUID per line. Lines preceded with # are ignored.",
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
@click.option(
|
||||
"--current-name",
|
||||
is_flag=True,
|
||||
@@ -1111,6 +1142,8 @@ def export(
|
||||
skip_bursts,
|
||||
skip_live,
|
||||
skip_raw,
|
||||
skip_uuid,
|
||||
skip_uuid_from_file,
|
||||
person_keyword,
|
||||
album_keyword,
|
||||
keyword_template,
|
||||
@@ -1188,6 +1221,7 @@ def export(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
query_eval,
|
||||
query_function,
|
||||
duplicate,
|
||||
@@ -1278,6 +1312,8 @@ def export(
|
||||
skip_bursts = cfg.skip_bursts
|
||||
skip_live = cfg.skip_live
|
||||
skip_raw = cfg.skip_raw
|
||||
skip_uuid = cfg.skip_uuid
|
||||
skip_uuid_from_file = cfg.skip_uuid_from_file
|
||||
person_keyword = cfg.person_keyword
|
||||
album_keyword = cfg.album_keyword
|
||||
keyword_template = cfg.keyword_template
|
||||
@@ -1352,6 +1388,7 @@ def export(
|
||||
max_size = cfg.max_size
|
||||
regex = cfg.regex
|
||||
selected = cfg.selected
|
||||
exif = cfg.exif
|
||||
query_eval = cfg.query_eval
|
||||
query_function = cfg.query_function
|
||||
duplicate = cfg.duplicate
|
||||
@@ -1671,6 +1708,7 @@ def export(
|
||||
max_size=max_size,
|
||||
regex=regex,
|
||||
selected=selected,
|
||||
exif=exif,
|
||||
query_eval=query_eval,
|
||||
function=query_function,
|
||||
duplicate=duplicate,
|
||||
@@ -1687,6 +1725,13 @@ def export(
|
||||
else:
|
||||
raise ValueError(e)
|
||||
|
||||
if skip_uuid:
|
||||
photos = [p for p in photos if p.uuid not in skip_uuid]
|
||||
|
||||
if skip_uuid_from_file:
|
||||
skip_uuid_list = load_uuid_from_file(skip_uuid_from_file)
|
||||
photos = [p for p in photos if p.uuid not in skip_uuid_list]
|
||||
|
||||
if photos and only_new:
|
||||
# ignore previously exported files
|
||||
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
|
||||
@@ -2083,6 +2128,7 @@ def query(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
query_eval,
|
||||
query_function,
|
||||
add_to_album,
|
||||
@@ -2118,6 +2164,7 @@ def query(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
duplicate,
|
||||
]
|
||||
exclusive = [
|
||||
@@ -2249,6 +2296,7 @@ def query(
|
||||
function=query_function,
|
||||
regex=regex,
|
||||
selected=selected,
|
||||
exif=exif,
|
||||
duplicate=duplicate,
|
||||
)
|
||||
|
||||
@@ -2968,7 +3016,7 @@ def export_photo_to_directory(
|
||||
break
|
||||
else:
|
||||
click.echo(
|
||||
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(
|
||||
@@ -3357,11 +3405,13 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
||||
Returns:
|
||||
tuple of (list of files deleted, list of directories deleted)
|
||||
"""
|
||||
keepers = {str(filename).lower(): 1 for filename in files_to_keep}
|
||||
keepers = {
|
||||
normalize_fs_path(str(filename).lower()): 1 for filename in files_to_keep
|
||||
}
|
||||
|
||||
deleted_files = []
|
||||
for p in pathlib.Path(dest_path).rglob("*"):
|
||||
path = str(p).lower()
|
||||
path = normalize_fs_path(str(p).lower())
|
||||
if p.is_file() and path not in keepers:
|
||||
verbose_(f"Deleting {p}")
|
||||
fileutil.unlink(p)
|
||||
@@ -3601,6 +3651,30 @@ def run_post_command(
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("packages", nargs=-1, required=True)
|
||||
@click.option(
|
||||
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
|
||||
)
|
||||
def install(packages, upgrade):
|
||||
"""Install Python packages into the same environment as osxphotos"""
|
||||
args = ["pip", "install"]
|
||||
if upgrade:
|
||||
args += ["--upgrade"]
|
||||
args += list(packages)
|
||||
sys.argv = args
|
||||
run_module("pip", run_name="__main__")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("packages", nargs=-1, required=True)
|
||||
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
|
||||
def uninstall(packages, yes):
|
||||
"""Uninstall Python packages from the osxphotos environment"""
|
||||
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
|
||||
run_module("pip", run_name="__main__")
|
||||
|
||||
|
||||
@cli.command(hidden=True)
|
||||
@DB_OPTION
|
||||
@DB_ARGUMENT
|
||||
@@ -3614,7 +3688,8 @@ def run_post_command(
|
||||
@click.option(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
help="Use with '--dump photos' to dump only certain UUIDs",
|
||||
help="Use with '--dump photos' to dump only certain UUIDs. "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@@ -4011,6 +4086,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
osxphotos uses the following 3rd party software licensed under the BSD-3-Clause License:
|
||||
Click (Copyright 2014 Pallets), ptpython (Copyright (c) 2015, Jonathan Slenders)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are
|
||||
permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list
|
||||
of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
of conditions and the following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
click.echo(f"osxphotos, version {__version__}")
|
||||
click.echo("")
|
||||
@@ -4032,7 +4133,7 @@ def tutorial(ctx, cli_obj, width):
|
||||
click.echo_via_pager(tutorial_help(width=width))
|
||||
|
||||
|
||||
def _show_photo(photo):
|
||||
def _show_photo(photo: PhotoInfo):
|
||||
"""open image with default image viewer
|
||||
|
||||
Note: This is for debugging only -- it will actually open any filetype which could
|
||||
@@ -4078,16 +4179,38 @@ def _get_selected(photosdb):
|
||||
return get_selected
|
||||
|
||||
|
||||
def _spotlight_photo(photo: PhotoInfo):
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
photo_.spotlight()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@DB_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def repl(ctx, cli_obj, db):
|
||||
"""Run interactive osxphotos shell"""
|
||||
@click.option(
|
||||
"--emacs",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Launch REPL with Emacs keybindings (default is vi bindings)",
|
||||
)
|
||||
def repl(ctx, cli_obj, db, emacs):
|
||||
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
|
||||
import logging
|
||||
|
||||
from osxphotos import PhotosDB, PhotoInfo, ExifTool
|
||||
from objexplore import explore
|
||||
from photoscript import Album, Photo, PhotosLibrary
|
||||
from rich import inspect as _inspect
|
||||
|
||||
from osxphotos import ExifTool, PhotoInfo, PhotosDB
|
||||
from osxphotos.albuminfo import AlbumInfo
|
||||
from osxphotos.placeinfo import PlaceInfo
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.disabled = True
|
||||
|
||||
pretty.install()
|
||||
print(f"python version: {sys.version}")
|
||||
print(f"osxphotos version: {osxphotos._version.__version__}")
|
||||
@@ -4102,24 +4225,21 @@ def repl(ctx, cli_obj, db):
|
||||
# shortcut for helper functions
|
||||
get_photo = photosdb.get_photo
|
||||
show = _show_photo
|
||||
spotlight = _spotlight_photo
|
||||
get_selected = _get_selected(photosdb)
|
||||
selected = get_selected()
|
||||
try:
|
||||
selected = get_selected()
|
||||
except Exception:
|
||||
# get_selected sometimes fails
|
||||
selected = []
|
||||
|
||||
def inspect(obj):
|
||||
"""inspect object"""
|
||||
return _inspect(obj, methods=True)
|
||||
|
||||
class ReprQuit:
|
||||
def __repr__(self):
|
||||
sys.exit(0)
|
||||
|
||||
def __call__(self):
|
||||
sys.exit(0)
|
||||
|
||||
quit = ReprQuit()
|
||||
q = ReprQuit()
|
||||
|
||||
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds")
|
||||
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
|
||||
print("The following classes have been imported from osxphotos:")
|
||||
print("- AlbumInfo, ExifTool, PhotoInfo, PhotosDB, PlaceInfo, QueryOptions\n")
|
||||
print("The following variables are defined:")
|
||||
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
|
||||
print(
|
||||
@@ -4129,16 +4249,34 @@ def repl(ctx, cli_obj, db):
|
||||
f"- selected: list of PhotoInfo objects for any photos selected in Photos (len={len(selected)})"
|
||||
)
|
||||
print(f"\nThe following functions may be helpful:")
|
||||
print(f"- get_photo(uuid): return a PhotoInfo object for photo with uuid")
|
||||
print(
|
||||
f"- get_selected(): return list of PhotoInfo objects for photos selected in Photos"
|
||||
)
|
||||
print(f"- show(photo): open a photo object in the default viewer")
|
||||
print(
|
||||
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
|
||||
f"- get_photo(uuid): return a PhotoInfo object for photo with uuid; e.g. get_photo('B13F4485-94E0-41CD-AF71-913095D62E31')"
|
||||
)
|
||||
print(
|
||||
f"- inspect(object): print information about an object; for example inspect(photosdb)"
|
||||
f"- get_selected(); return list of PhotoInfo objects for photos selected in Photos"
|
||||
)
|
||||
print(
|
||||
f"- show(photo): open a photo object in the default viewer; e.g. show(selected[0])"
|
||||
)
|
||||
print(
|
||||
f"- show(path): open a file at path in the default viewer; e.g. show('/path/to/photo.jpg')"
|
||||
)
|
||||
print(f"- spotlight(photo): open a photo and spotlight it in Photos")
|
||||
# print(
|
||||
# f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
|
||||
# )
|
||||
print(
|
||||
f"- inspect(object): print information about an object; e.g. inspect(PhotoInfo)"
|
||||
)
|
||||
print(
|
||||
f"- explore(object): interactively explore an object with objexplore; e.g. explore(PhotoInfo)"
|
||||
)
|
||||
print(f"- q, quit, quit(), exit, exit(): exit this interactive shell\n")
|
||||
|
||||
embed_repl(
|
||||
globals=globals(),
|
||||
locals=locals(),
|
||||
history_filename=str(pathlib.Path.home() / ".osxphotos_repl_history"),
|
||||
quit_words=["q", "quit", "exit"],
|
||||
vi_mode=not emacs,
|
||||
)
|
||||
print(f"- q, quit, or quit(): exit this interactive shell\n")
|
||||
code.interact(banner="", local=locals())
|
||||
|
||||
@@ -207,7 +207,7 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "The following categories are available: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Catgory", "Description")]
|
||||
templ_tuples = [("Category", "Description")]
|
||||
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
formatter.write("\n")
|
||||
@@ -224,13 +224,13 @@ The following attributes may be used with '--xattr-template':
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
'--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
|
||||
'--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text(
|
||||
"In the above command, the 'shell_quote' filter is used to ensure '{filepath.name}' is properly quoted "
|
||||
"In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is properly quoted "
|
||||
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
|
||||
"If '{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
|
||||
"If '{filepath}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
|
||||
"thus renders to: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
|
||||
@@ -7,7 +7,7 @@ import subprocess
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
|
||||
from .imageconverter import ImageConverter
|
||||
|
||||
@@ -114,7 +114,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
if dest.is_dir():
|
||||
dest /= src.name
|
||||
|
||||
filemgr = CoreFoundation.NSFileManager.defaultManager()
|
||||
filemgr = Foundation.NSFileManager.defaultManager()
|
||||
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
|
||||
# error is a tuple of (bool, error_string)
|
||||
# error[0] is True if copy succeeded
|
||||
|
||||
69
osxphotos/momentinfo.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""MomentInfo class with details about photo moments."""
|
||||
|
||||
|
||||
class MomentInfo:
|
||||
"""Info about a photo moment"""
|
||||
|
||||
def __init__(self, db, moment_pk):
|
||||
"""Initialize with a moment PK; returns None if PK not found."""
|
||||
self._db = db
|
||||
self._pk = moment_pk
|
||||
|
||||
self._moment = self._db._db_moment_pk.get(moment_pk)
|
||||
if not self._moment:
|
||||
raise ValueError(f"No moment with PK {moment_pk}")
|
||||
|
||||
@property
|
||||
def pk(self):
|
||||
"""Primary key of the moment."""
|
||||
return self._pk
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
"""Location of the moment."""
|
||||
return (self._moment.get("latitude"), self._moment.get("longitude"))
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Title of the moment."""
|
||||
return self._moment.get("title")
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
"""Subtitle of the moment."""
|
||||
return self._moment.get("subtitle")
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
"""Start date of the moment."""
|
||||
return self._moment.get("startDate")
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
"""Stop date of the moment."""
|
||||
return self._moment.get("endDate")
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""Date of the moment."""
|
||||
return self._moment.get("representativeDate")
|
||||
|
||||
@property
|
||||
def modification_date(self):
|
||||
"""Modification date of the moment."""
|
||||
return self._moment.get("modificationDate")
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
"""All photos in this moment"""
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
photo_uuids = [
|
||||
uuid
|
||||
for uuid, photo in self._db._dbphotos.items()
|
||||
if photo["momentID"] == self._pk
|
||||
]
|
||||
|
||||
self._photos = self._db.photos_by_uuid(photo_uuids)
|
||||
return self._photos
|
||||
@@ -141,7 +141,7 @@ class FaceInfo:
|
||||
self.manual = face["manual"]
|
||||
self.face_type = face["facetype"]
|
||||
self.age_type = face["agetype"]
|
||||
self.bald_type = face["baldtype"]
|
||||
# self.bald_type = face["baldtype"]
|
||||
self.eye_makeup_type = face["eyemakeuptype"]
|
||||
self.eye_state = face["eyestate"]
|
||||
self.facial_hair_type = face["facialhairtype"]
|
||||
@@ -438,7 +438,7 @@ class FaceInfo:
|
||||
"manual": self.manual,
|
||||
"face_type": self.face_type,
|
||||
"age_type": self.age_type,
|
||||
"bald_type": self.bald_type,
|
||||
# "bald_type": self.bald_type,
|
||||
"eye_makeup_type": self.eye_makeup_type,
|
||||
"eye_state": self.eye_state,
|
||||
"facial_hair_type": self.facial_hair_type,
|
||||
|
||||
@@ -56,7 +56,7 @@ from ..photokit import (
|
||||
)
|
||||
from ..phototemplate import RenderOptions
|
||||
from ..uti import get_preferred_uti_extension
|
||||
from ..utils import findfiles, lineno, noop
|
||||
from ..utils import increment_filename, increment_filename_with_count, lineno
|
||||
|
||||
# retry if use_photos_export fails the first time (which sometimes it does)
|
||||
MAX_PHOTOSCRIPT_RETRIES = 3
|
||||
@@ -280,7 +280,7 @@ def _export_photo_uuid_applescript(
|
||||
|
||||
if not exported_files or not filename:
|
||||
# nothing got exported
|
||||
raise ExportError(f"Could not export photo {uuid}")
|
||||
raise ExportError(f"Could not export photo {uuid} ({lineno(__file__)})")
|
||||
|
||||
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
|
||||
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
|
||||
@@ -683,15 +683,12 @@ def export2(
|
||||
# e.g. exporting sidecar for file1.png and file1.jpeg
|
||||
# if file1.png exists and exporting file1.jpeg,
|
||||
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
||||
count = 0
|
||||
if not update and increment and not overwrite:
|
||||
dest_files = findfiles(f"{dest_original.stem}*", str(dest_original.parent))
|
||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||
dest_new = dest_original.stem
|
||||
while dest_new.lower() in dest_files:
|
||||
count += 1
|
||||
dest_new = f"{dest_original.stem} ({count})"
|
||||
dest_original = dest_original.parent / f"{dest_new}{dest_original.suffix}"
|
||||
increment_file_count = 0
|
||||
if increment and not update and not overwrite:
|
||||
dest_original, increment_file_count = increment_filename_with_count(
|
||||
dest_original
|
||||
)
|
||||
dest_original = pathlib.Path(dest_original)
|
||||
|
||||
# if overwrite==False and #increment==False, export should fail if file exists
|
||||
if (
|
||||
@@ -706,17 +703,11 @@ def export2(
|
||||
)
|
||||
|
||||
if export_edited:
|
||||
if not update and increment and not overwrite:
|
||||
dest_files = findfiles(f"{dest_edited.stem}*", str(dest_edited.parent))
|
||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||
dest_new = dest_edited.stem
|
||||
if count:
|
||||
# incremented above when checking original destination
|
||||
dest_new = f"{dest_new} ({count})"
|
||||
while dest_new.lower() in dest_files:
|
||||
count += 1
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
dest_edited = dest_edited.parent / f"{dest_new}{dest_edited.suffix}"
|
||||
if increment and not update and not overwrite:
|
||||
dest_edited, increment_file_count = increment_filename_with_count(
|
||||
dest_edited, increment_file_count
|
||||
)
|
||||
dest_edited = pathlib.Path(dest_edited)
|
||||
|
||||
# if overwrite==False and #increment==False, export should fail if file exists
|
||||
if dest_edited.exists() and not update and not overwrite and not increment:
|
||||
@@ -800,20 +791,16 @@ def export2(
|
||||
)
|
||||
if dest_uuid != self.uuid:
|
||||
# not the right file, find the right one
|
||||
count = 1
|
||||
glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}")
|
||||
dest_files = glob.glob(glob_str)
|
||||
found_match = False
|
||||
for file_ in dest_files:
|
||||
dest_uuid = export_db.get_uuid_for_file(file_)
|
||||
if dest_uuid == self.uuid:
|
||||
dest = pathlib.Path(file_)
|
||||
found_match = True
|
||||
break
|
||||
elif dest_uuid is None and fileutil.cmp(src, file_):
|
||||
# files match, update the UUID
|
||||
dest = pathlib.Path(file_)
|
||||
found_match = True
|
||||
export_db.set_data(
|
||||
filename=dest,
|
||||
uuid=self.uuid,
|
||||
@@ -825,18 +812,14 @@ def export2(
|
||||
exif_json=None,
|
||||
)
|
||||
break
|
||||
|
||||
if not found_match:
|
||||
else:
|
||||
# increment the destination file
|
||||
count = 1
|
||||
glob_str = str(dest.parent / f"{dest.stem}*")
|
||||
dest_files = glob.glob(glob_str)
|
||||
dest_files = [pathlib.Path(f).stem for f in dest_files]
|
||||
dest_new = dest.stem
|
||||
while dest_new in dest_files:
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
count += 1
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
dest = pathlib.Path(increment_filename(dest))
|
||||
|
||||
if export_original:
|
||||
dest_original = dest
|
||||
else:
|
||||
dest_edited = dest
|
||||
|
||||
# export the dest file
|
||||
results = self._export_photo(
|
||||
@@ -929,6 +912,14 @@ def export2(
|
||||
preview_path = pathlib.Path(self.path_derivatives[0])
|
||||
preview_ext = preview_path.suffix
|
||||
preview_name = dest.parent / f"{dest.stem}{preview_suffix}{preview_ext}"
|
||||
# if original is missing, the filename won't have been incremented so
|
||||
# need to check here to make sure there aren't duplicate preview files in
|
||||
# the export directory
|
||||
preview_name = (
|
||||
preview_name
|
||||
if overwrite or update
|
||||
else pathlib.Path(increment_filename(preview_name))
|
||||
)
|
||||
if preview_path is not None:
|
||||
results = self._export_photo(
|
||||
preview_path,
|
||||
@@ -1275,6 +1266,7 @@ def _export_photo_with_photos_export(
|
||||
dest.name,
|
||||
version=PHOTOS_VERSION_CURRENT,
|
||||
overwrite=overwrite,
|
||||
video=live_photo,
|
||||
)
|
||||
all_results.exported.extend(exported)
|
||||
except Exception as e:
|
||||
@@ -1322,6 +1314,7 @@ def _export_photo_with_photos_export(
|
||||
dest.name,
|
||||
version=PHOTOS_VERSION_ORIGINAL,
|
||||
overwrite=overwrite,
|
||||
video=live_photo,
|
||||
)
|
||||
all_results.exported.extend(exported)
|
||||
except Exception as e:
|
||||
@@ -1531,16 +1524,31 @@ def _export_photo(
|
||||
edited_stat = fileutil.file_sig(src) if edited else (None, None, None)
|
||||
if dest_exists and (update or overwrite):
|
||||
# need to remove the destination first
|
||||
fileutil.unlink(dest)
|
||||
try:
|
||||
fileutil.unlink(dest)
|
||||
except Exception as e:
|
||||
raise ExportError(
|
||||
f"Error removing file {dest}: {e} (({lineno(__file__)})"
|
||||
) from e
|
||||
if export_as_hardlink:
|
||||
fileutil.hardlink(src, dest)
|
||||
try:
|
||||
fileutil.hardlink(src, dest)
|
||||
except Exception as e:
|
||||
raise ExportError(
|
||||
f"Error hardlinking {src} to {dest}: {e} ({lineno(__file__)})"
|
||||
) from e
|
||||
elif convert_to_jpeg:
|
||||
# use convert_to_jpeg to export the file
|
||||
fileutil.convert_to_jpeg(src, dest_str, compression_quality=jpeg_quality)
|
||||
converted_stat = fileutil.file_sig(dest_str)
|
||||
converted_to_jpeg_files.append(dest_str)
|
||||
else:
|
||||
fileutil.copy(src, dest_str)
|
||||
try:
|
||||
fileutil.copy(src, dest_str)
|
||||
except Exception as e:
|
||||
raise ExportError(
|
||||
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
|
||||
) from e
|
||||
|
||||
export_db.set_data(
|
||||
filename=dest_str,
|
||||
|
||||
@@ -20,10 +20,14 @@ from .._constants import (
|
||||
_MOVIE_TYPE,
|
||||
_PHOTO_TYPE,
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_ALBUM_TYPE_ALBUM,
|
||||
_PHOTOS_4_ALBUM_TYPE_PROJECT,
|
||||
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
|
||||
_PHOTOS_4_ROOT_FOLDER,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
@@ -34,10 +38,12 @@ from .._constants import (
|
||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
||||
)
|
||||
from ..adjustmentsinfo import AdjustmentsInfo
|
||||
from ..albuminfo import AlbumInfo, ImportInfo
|
||||
from ..albuminfo import AlbumInfo, ImportInfo, ProjectInfo
|
||||
from ..momentinfo import MomentInfo
|
||||
from ..personinfo import FaceInfo, PersonInfo
|
||||
from ..phototemplate import PhotoTemplate, RenderOptions
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..query_builder import get_query
|
||||
from ..text_detection import detect_text
|
||||
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
||||
from ..utils import _debug, _get_resource_loc, findfiles
|
||||
@@ -493,6 +499,18 @@ class PhotoInfo:
|
||||
self._faceinfo = []
|
||||
return self._faceinfo
|
||||
|
||||
@property
|
||||
def moment(self):
|
||||
"""Moment photo belongs to"""
|
||||
try:
|
||||
return self._moment
|
||||
except AttributeError:
|
||||
try:
|
||||
self._moment = MomentInfo(db=self._db, moment_pk=self._info["momentID"])
|
||||
except ValueError:
|
||||
self._moment = None
|
||||
return self._moment
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
"""list of albums picture is contained in"""
|
||||
@@ -556,6 +574,18 @@ class PhotoInfo:
|
||||
)
|
||||
return self._import_info
|
||||
|
||||
@property
|
||||
def project_info(self):
|
||||
"""list of AlbumInfo objects representing projects for the photo or None if no projects"""
|
||||
try:
|
||||
return self._project_info
|
||||
except AttributeError:
|
||||
project_uuids = self._get_album_uuids(project=True)
|
||||
self._project_info = [
|
||||
ProjectInfo(db=self._db, uuid=album) for album in project_uuids
|
||||
]
|
||||
return self._project_info
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
"""list of keywords for picture"""
|
||||
@@ -564,7 +594,12 @@ class PhotoInfo:
|
||||
@property
|
||||
def title(self):
|
||||
"""name / title of picture"""
|
||||
return self._info["name"]
|
||||
# if user sets then deletes title, Photos sets it to empty string in DB instead of NULL
|
||||
# in this case, return None so result is the same as if title had never been set (which returns NULL)
|
||||
# issue #512
|
||||
title = self._info["name"]
|
||||
title = None if title == "" else title
|
||||
return title
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
@@ -837,7 +872,7 @@ class PhotoInfo:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.live_photo and not self.ismissing:
|
||||
live_model_id = self._info["live_model_id"]
|
||||
if live_model_id == None:
|
||||
if live_model_id is None:
|
||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||
photopath = None
|
||||
else:
|
||||
@@ -858,28 +893,20 @@ class PhotoInfo:
|
||||
# photos 4 has "isOnDisk" column we could check
|
||||
# or could do the actual check with "isfile"
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
else:
|
||||
# Photos 5
|
||||
if self.live_photo and not self.ismissing:
|
||||
filename = pathlib.Path(self.path)
|
||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||
photopath = str(photopath)
|
||||
if not os.path.isfile(photopath):
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
elif self.live_photo and self.path and not self.ismissing:
|
||||
filename = pathlib.Path(self.path)
|
||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||
photopath = str(photopath)
|
||||
if not os.path.isfile(photopath):
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||
# TODO: should this be a warning or debug?
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
|
||||
return photopath
|
||||
|
||||
@@ -1049,15 +1076,15 @@ class PhotoInfo:
|
||||
return self._info["orientation"]
|
||||
|
||||
# For Photos 5+, try to get the adjusted orientation
|
||||
if self.hasadjustments:
|
||||
if self.adjustments:
|
||||
return self.adjustments.adj_orientation
|
||||
else:
|
||||
# can't reliably determine orientation for edited photo if adjustmentinfo not available
|
||||
return 0
|
||||
else:
|
||||
if not self.hasadjustments:
|
||||
return self._info["orientation"]
|
||||
|
||||
if self.adjustments:
|
||||
return self.adjustments.adj_orientation
|
||||
else:
|
||||
# can't reliably determine orientation for edited photo if adjustmentinfo not available
|
||||
return 0
|
||||
|
||||
@property
|
||||
def original_height(self):
|
||||
"""returns height of the original photo version in pixels"""
|
||||
@@ -1093,6 +1120,26 @@ class PhotoInfo:
|
||||
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
|
||||
return duplicates
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""Return name of photo owner for shared photos (Photos 5+ only), or None if not shared"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._owner
|
||||
except AttributeError:
|
||||
try:
|
||||
personid = self._info["cloudownerhashedpersonid"]
|
||||
self._owner = (
|
||||
self._db._db_hashed_person_id[personid]["full_name"]
|
||||
if personid
|
||||
else None
|
||||
)
|
||||
except KeyError:
|
||||
self._owner = None
|
||||
return self._owner
|
||||
|
||||
def render_template(
|
||||
self, template_str: str, options: Optional[RenderOptions] = None
|
||||
):
|
||||
@@ -1151,7 +1198,8 @@ class PhotoInfo:
|
||||
md = OSXMetaData(path)
|
||||
detected_text = md.get_attribute("osxphotos_detected_text")
|
||||
if detected_text is None:
|
||||
detected_text = detect_text(path)
|
||||
orientation = self.orientation or None
|
||||
detected_text = detect_text(path, orientation)
|
||||
md.set_attribute("osxphotos_detected_text", detected_text)
|
||||
return detected_text
|
||||
|
||||
@@ -1165,34 +1213,48 @@ class PhotoInfo:
|
||||
"""Returns latitude, in degrees"""
|
||||
return self._info["latitude"]
|
||||
|
||||
def _get_album_uuids(self):
|
||||
def _get_album_uuids(self, project=False):
|
||||
"""Return list of album UUIDs this photo is found in
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
|
||||
if project is True, returns special "My Project" albums (e.g. cards, calendars, slideshows)
|
||||
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
version4 = True
|
||||
album_kind = [_PHOTOS_4_ALBUM_KIND]
|
||||
else:
|
||||
version4 = False
|
||||
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
|
||||
album_type = (
|
||||
[_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW]
|
||||
if project
|
||||
else [_PHOTOS_4_ALBUM_TYPE_ALBUM]
|
||||
)
|
||||
album_list = []
|
||||
for album in self._info["albums"]:
|
||||
detail = self._db._dbalbum_details[album]
|
||||
if (
|
||||
detail["kind"] in album_kind
|
||||
and detail["albumType"] in album_type
|
||||
and not detail["intrash"]
|
||||
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
|
||||
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||
# but should not be listed here; they can be distinguished by looking
|
||||
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
):
|
||||
album_list.append(album)
|
||||
return album_list
|
||||
|
||||
# Photos 5+
|
||||
album_kind = (
|
||||
[_PHOTOS_5_PROJECT_ALBUM_KIND]
|
||||
if project
|
||||
else [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
|
||||
)
|
||||
|
||||
album_list = []
|
||||
for album in self._info["albums"]:
|
||||
detail = self._db._dbalbum_details[album]
|
||||
if (
|
||||
detail["kind"] in album_kind
|
||||
and not detail["intrash"]
|
||||
and (
|
||||
not version4
|
||||
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||
# but should not be listed here; they can be distinguished by looking
|
||||
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
|
||||
)
|
||||
):
|
||||
if detail["kind"] in album_kind and not detail["intrash"]:
|
||||
album_list.append(album)
|
||||
return album_list
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"""
|
||||
|
||||
# NOTES:
|
||||
# - This likely leaks memory like a sieve as I need to ensure all the
|
||||
# Objective C objects are cleaned up.
|
||||
# - There are several techniques used for handling PhotoKit's various
|
||||
# asynchronous calls used in this code: event loop+notification, threading
|
||||
# event, while loop. I've experimented with each to find the one that works.
|
||||
@@ -200,16 +198,6 @@ class PHAssetResourceData:
|
||||
self.data = b""
|
||||
|
||||
|
||||
# class LivePhotoData:
|
||||
# """ Simple class to hold the data passed to the handler for
|
||||
# requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:
|
||||
# """
|
||||
|
||||
# def __init__(self):
|
||||
# self.live_photo = None
|
||||
# self.info = None
|
||||
|
||||
|
||||
class PhotoKitNotificationDelegate(NSObject):
|
||||
"""Handles notifications from NotificationCenter;
|
||||
used with asynchronous PhotoKit requests to stop event loop when complete
|
||||
@@ -487,6 +475,7 @@ class PhotoAsset:
|
||||
version=PHOTOS_VERSION_CURRENT,
|
||||
overwrite=False,
|
||||
raw=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Export image to path
|
||||
|
||||
@@ -496,6 +485,7 @@ class PhotoAsset:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
raw: bool, if True, export RAW component of RAW+JPEG pair, default is False
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
List of path to exported image(s)
|
||||
@@ -504,9 +494,6 @@ class PhotoAsset:
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
|
||||
# if self.live:
|
||||
# raise NotImplementedError("Live photos not implemented yet")
|
||||
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
@@ -615,9 +602,7 @@ class PhotoAsset:
|
||||
|
||||
nonlocal requestdata
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
@@ -701,9 +686,7 @@ class PhotoAsset:
|
||||
|
||||
nonlocal data
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
data.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
@@ -789,7 +772,6 @@ class SlowMoVideoExporter(NSObject):
|
||||
self.url = None
|
||||
self.done = None
|
||||
self.nc = None
|
||||
# super(NSObject, self).dealloc()
|
||||
|
||||
|
||||
class VideoAsset(PhotoAsset):
|
||||
@@ -801,7 +783,12 @@ class VideoAsset(PhotoAsset):
|
||||
# https://developer.apple.com/documentation/photokit/phimagemanager/1616981-requestexportsessionforvideo?language=objc
|
||||
# above 10.15 only
|
||||
def export(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
self,
|
||||
dest,
|
||||
filename=None,
|
||||
version=PHOTOS_VERSION_CURRENT,
|
||||
overwrite=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Export video to path
|
||||
|
||||
@@ -810,6 +797,7 @@ class VideoAsset(PhotoAsset):
|
||||
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
List of path to exported image(s)
|
||||
@@ -1043,6 +1031,7 @@ class LivePhotoAsset(PhotoAsset):
|
||||
overwrite=False,
|
||||
photo=True,
|
||||
video=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Export image to path
|
||||
|
||||
@@ -1053,6 +1042,7 @@ class LivePhotoAsset(PhotoAsset):
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
photo: bool, if True, export photo component of live photo
|
||||
video: bool, if True, export live video component of live photo
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
list of [path to exported image and/or video]
|
||||
@@ -1104,39 +1094,6 @@ class LivePhotoAsset(PhotoAsset):
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# exported = []
|
||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||
# causes pathlib.Path().is_file() to fail in tests
|
||||
|
||||
# if photo:
|
||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# photo_resource, photo_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(photo_output_file))
|
||||
|
||||
# if video:
|
||||
# video_output_url = path_to_NSURL(video_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# video_resource, video_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(video_output_file))
|
||||
|
||||
# def completion_handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
@@ -1155,41 +1112,6 @@ class LivePhotoAsset(PhotoAsset):
|
||||
request.dealloc()
|
||||
return exported
|
||||
|
||||
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
|
||||
# # Returns an NSImage which isn't overly useful
|
||||
# # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc
|
||||
|
||||
# # requestImageForAsset:targetSize:contentMode:options:resultHandler:
|
||||
|
||||
# options = Photos.PHImageRequestOptions.alloc().init()
|
||||
# options.setVersion_(version)
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# options.setSynchronous_(True)
|
||||
# options.setDeliveryMode_(
|
||||
# Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
# )
|
||||
|
||||
# event = threading.Event()
|
||||
# image_data = ImageData()
|
||||
|
||||
# def handler(result, info):
|
||||
# nonlocal image_data
|
||||
# if not info["PHImageResultIsDegradedKey"]:
|
||||
# image_data.image_data = result
|
||||
# image_data.info = info
|
||||
# event.set()
|
||||
|
||||
# self._manager.requestImageForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
# self._phasset,
|
||||
# Photos.PHImageManagerMaximumSize,
|
||||
# Photos.PHImageContentModeDefault,
|
||||
# options,
|
||||
# handler,
|
||||
# )
|
||||
# event.wait()
|
||||
# options.dealloc()
|
||||
# return image_data
|
||||
|
||||
|
||||
class PhotoLibrary:
|
||||
"""Interface to PhotoKit PHImageManager and PHPhotoLibrary"""
|
||||
|
||||
@@ -70,12 +70,24 @@ def _process_comments_5(photosdb):
|
||||
results = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT
|
||||
ZINVITEEHASHEDPERSONID,
|
||||
ZINVITEEFIRSTNAME,
|
||||
ZINVITEELASTNAME,
|
||||
ZINVITEEFULLNAME
|
||||
FROM
|
||||
ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||
ZINVITEEHASHEDPERSONID AS HASHEDPERSONID,
|
||||
ZINVITEEFIRSTNAME AS FIRSTNAME,
|
||||
ZINVITEELASTNAME AS LASTNAME,
|
||||
ZINVITEEFULLNAME AS FULLNAME
|
||||
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||
WHERE HASHEDPERSONID IS NOT NULL
|
||||
AND HASHEDPERSONID != ""
|
||||
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
|
||||
UNION
|
||||
SELECT DISTINCT
|
||||
ZCLOUDOWNERHASHEDPERSONID AS HASHEDPERSONID,
|
||||
ZCLOUDOWNERFIRSTNAME AS FIRSTNAME,
|
||||
ZCLOUDOWNERLASTNAME AS LASTNAME,
|
||||
ZCLOUDOWNERFULLNAME AS FULLNAME
|
||||
FROM ZGENERICALBUM
|
||||
WHERE HASHEDPERSONID IS NOT NULL
|
||||
AND HASHEDPERSONID != ""
|
||||
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -148,10 +160,10 @@ def _process_comments_5(photosdb):
|
||||
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
|
||||
|
||||
# sort results
|
||||
for uuid in photosdb._db_comments_uuid:
|
||||
for uuid, value in photosdb._db_comments_uuid.items():
|
||||
if photosdb._db_comments_uuid[uuid]["likes"]:
|
||||
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
|
||||
if photosdb._db_comments_uuid[uuid]["comments"]:
|
||||
photosdb._db_comments_uuid[uuid]["comments"].sort(key=lambda x: x.datetime)
|
||||
value["comments"].sort(key=lambda x: x.datetime)
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -146,7 +146,6 @@ def _process_faceinfo_4(photosdb):
|
||||
|
||||
# Photos 5 only
|
||||
face["agetype"] = None
|
||||
face["baldtype"] = None
|
||||
face["eyemakeuptype"] = None
|
||||
face["eyestate"] = None
|
||||
face["facialhairtype"] = None
|
||||
@@ -194,7 +193,7 @@ def _process_faceinfo_5(photosdb):
|
||||
ZDETECTEDFACE.ZPERSON,
|
||||
ZPERSON.ZFULLNAME,
|
||||
ZDETECTEDFACE.ZAGETYPE,
|
||||
ZDETECTEDFACE.ZBALDTYPE,
|
||||
NULL, -- ZDETECTEDFACE.ZBALDTYPE (Removed in Monterey)
|
||||
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||
ZDETECTEDFACE.ZEYESSTATE,
|
||||
ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||
@@ -239,7 +238,7 @@ def _process_faceinfo_5(photosdb):
|
||||
# 3 ZDETECTEDFACE.ZPERSON,
|
||||
# 4 ZPERSON.ZFULLNAME,
|
||||
# 5 ZDETECTEDFACE.ZAGETYPE,
|
||||
# 6 ZDETECTEDFACE.ZBALDTYPE,
|
||||
# 6 ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
|
||||
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||
# 8 ZDETECTEDFACE.ZEYESSTATE,
|
||||
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||
@@ -284,7 +283,6 @@ def _process_faceinfo_5(photosdb):
|
||||
face["person"] = person_pk
|
||||
face["fullname"] = normalize_unicode(row[4])
|
||||
face["agetype"] = row[5]
|
||||
face["baldtype"] = row[6]
|
||||
face["eyemakeuptype"] = row[7]
|
||||
face["eyestate"] = row[8]
|
||||
face["facialhairtype"] = row[9]
|
||||
|
||||
@@ -12,12 +12,14 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pprint import pformat
|
||||
from typing import List
|
||||
|
||||
import bitmath
|
||||
import photoscript
|
||||
from rich import print
|
||||
|
||||
from .._constants import (
|
||||
_DB_TABLE_NAMES,
|
||||
@@ -26,11 +28,15 @@ from .._constants import (
|
||||
_PHOTOS_3_VERSION,
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_ROOT_FOLDER,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||
_PHOTOS_4_ALBUM_TYPE_ALBUM,
|
||||
_PHOTOS_4_ALBUM_TYPE_PROJECT,
|
||||
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_TESTED_OS_VERSIONS,
|
||||
@@ -40,7 +46,7 @@ from .._constants import (
|
||||
TIME_DELTA,
|
||||
)
|
||||
from .._version import __version__
|
||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
|
||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
|
||||
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||
from ..fileutil import FileUtil
|
||||
from ..personinfo import PersonInfo
|
||||
@@ -250,6 +256,10 @@ class PhotosDB:
|
||||
# Dict to hold information on volume names (Photos 5+)
|
||||
self._db_filesystem_volumes = {}
|
||||
|
||||
# Dict to hold information on moments (Photos 5+)
|
||||
# key is Z_PK of ZMOMENT table and values are the moment info
|
||||
self._db_moment_pk = {}
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
@@ -330,6 +340,8 @@ class PhotosDB:
|
||||
else:
|
||||
self._process_database5()
|
||||
|
||||
self._db_connection, _ = self.get_db_connection()
|
||||
|
||||
@property
|
||||
def keywords_as_dict(self):
|
||||
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
|
||||
@@ -421,7 +433,7 @@ class PhotosDB:
|
||||
for folder, detail in self._dbfolder_details.items()
|
||||
if not detail["intrash"]
|
||||
and not detail["isMagic"]
|
||||
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
and detail["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
]
|
||||
else:
|
||||
folders = [
|
||||
@@ -442,7 +454,7 @@ class PhotosDB:
|
||||
for folder in self._dbfolder_details.values()
|
||||
if not folder["intrash"]
|
||||
and not folder["isMagic"]
|
||||
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
and folder["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
]
|
||||
else:
|
||||
folder_names = [
|
||||
@@ -521,6 +533,18 @@ class PhotosDB:
|
||||
]
|
||||
return self._import_info
|
||||
|
||||
@property
|
||||
def project_info(self):
|
||||
"""return list of AlbumInfo projects for each project in the database"""
|
||||
try:
|
||||
return self._project_info
|
||||
except AttributeError:
|
||||
self._project_info = [
|
||||
ProjectInfo(db=self, uuid=album)
|
||||
for album in self._get_album_uuids(project=True)
|
||||
]
|
||||
return self._project_info
|
||||
|
||||
@property
|
||||
def db_version(self):
|
||||
"""return the database version as stored in LiGlobals table"""
|
||||
@@ -790,8 +814,8 @@ class PhotosDB:
|
||||
"creation_date": album[8],
|
||||
"start_date": None, # Photos 5 only
|
||||
"end_date": None, # Photos 5 only
|
||||
"customsortascending": None, # Photos 5 only
|
||||
"customsortkey": None, # Photos 5 only
|
||||
"customsortascending": None, # Photos 5 only
|
||||
"customsortkey": None, # Photos 5 only
|
||||
}
|
||||
|
||||
# get details about folders
|
||||
@@ -840,11 +864,10 @@ class PhotosDB:
|
||||
# build folder hierarchy
|
||||
for album, details in self._dbalbum_details.items():
|
||||
parent_folder = details["folderUuid"]
|
||||
if details[
|
||||
"albumSubclass"
|
||||
] == _PHOTOS_4_ALBUM_KIND and parent_folder not in [
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
]:
|
||||
if (
|
||||
details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
|
||||
and parent_folder not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
):
|
||||
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
|
||||
self._dbalbum_folders[album] = folder_hierarchy
|
||||
else:
|
||||
@@ -1104,7 +1127,9 @@ class PhotosDB:
|
||||
# get info on special types
|
||||
self._dbphotos[uuid]["specialType"] = row[25]
|
||||
self._dbphotos[uuid]["masterModelID"] = row[26]
|
||||
self._dbphotos[uuid]["pk"] = row[26] # same as masterModelID, to match Photos 5
|
||||
self._dbphotos[uuid]["pk"] = row[
|
||||
26
|
||||
] # same as masterModelID, to match Photos 5
|
||||
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
||||
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 else False
|
||||
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
|
||||
@@ -1195,6 +1220,9 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["import_uuid"] = row[44]
|
||||
self._dbphotos[uuid]["fok_import_session"] = None
|
||||
|
||||
# photos 5+ only, for shared photos
|
||||
self._dbphotos[uuid]["cloudownerhashedpersonid"] = None
|
||||
|
||||
# compute signatures for finding possible duplicates
|
||||
signature = self._duplicate_signature(uuid)
|
||||
try:
|
||||
@@ -1569,7 +1597,7 @@ class PhotosDB:
|
||||
if parent_uuid is None:
|
||||
return folders
|
||||
|
||||
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM:
|
||||
if parent_uuid in _PHOTOS_4_TOP_LEVEL_ALBUMS:
|
||||
if not folders:
|
||||
# this is a top-level folder with no sub-folders
|
||||
folders = {uuid: None}
|
||||
@@ -1923,7 +1951,8 @@ class PhotosDB:
|
||||
{asset_table}.ZTRASHEDDATE,
|
||||
{asset_table}.ZSAVEDASSETTYPE,
|
||||
{asset_table}.ZADDEDDATE,
|
||||
{asset_table}.Z_PK
|
||||
{asset_table}.Z_PK,
|
||||
{asset_table}.ZCLOUDOWNERHASHEDPERSONID
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1973,6 +2002,7 @@ class PhotosDB:
|
||||
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
||||
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
|
||||
# 42 ZGENERICASSET.Z_PK -- primary key
|
||||
# 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -2158,6 +2188,7 @@ class PhotosDB:
|
||||
info["added_date"] = datetime(1970, 1, 1)
|
||||
|
||||
info["pk"] = row[42]
|
||||
info["cloudownerhashedpersonid"] = row[43]
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
@@ -2481,6 +2512,10 @@ class PhotosDB:
|
||||
verbose("Processing comments and likes for shared photos.")
|
||||
self._process_comments()
|
||||
|
||||
# process moments
|
||||
verbose("Processing moments.")
|
||||
self._process_moments()
|
||||
|
||||
# done processing, dump debug data if requested
|
||||
verbose("Done processing details from Photos library.")
|
||||
if _debug():
|
||||
@@ -2526,6 +2561,109 @@ class PhotosDB:
|
||||
logging.debug("Burst Photos (dbphotos_burst:")
|
||||
logging.debug(pformat(self._dbphotos_burst))
|
||||
|
||||
def _process_moments(self):
|
||||
"""Process data from ZMOMENT table"""
|
||||
# _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
raise NotImplementedError(
|
||||
f"Moment info implemented for this database version"
|
||||
)
|
||||
else:
|
||||
self._process_moment_5()
|
||||
|
||||
def _process_moment_5(self):
|
||||
"""Process moment info for Photos 5 databases"""
|
||||
|
||||
self._db_moment_pk = {}
|
||||
|
||||
results = self.execute(
|
||||
f"""
|
||||
SELECT
|
||||
Z_PK,
|
||||
ZTIMEZONEOFFSET,
|
||||
ZTRASHEDSTATE,
|
||||
ZAPPROXIMATELATITUDE,
|
||||
ZAPPROXIMATELONGITUDE,
|
||||
ZENDDATE,
|
||||
ZMODIFICATIONDATE,
|
||||
ZREPRESENTATIVEDATE,
|
||||
ZSTARTDATE,
|
||||
ZSUBTITLE,
|
||||
ZTITLE,
|
||||
ZUUID
|
||||
FROM ZMOMENT"""
|
||||
)
|
||||
|
||||
# results
|
||||
# 0 Z_PK,
|
||||
# 1 ZTIMEZONEOFFSET,
|
||||
# 2 ZTRASHEDSTATE,
|
||||
# 3 ZAPPROXIMATELATITUDE,
|
||||
# 4 ZAPPROXIMATELONGITUDE,
|
||||
# 5 ZENDDATE,
|
||||
# 6 ZMODIFICATIONDATE,
|
||||
# 7 ZREPRESENTATIVEDATE,
|
||||
# 8 ZSTARTDATE,
|
||||
# 9 ZSUBTITLE,
|
||||
# 10 ZTITLE,
|
||||
# 11 ZUUID
|
||||
|
||||
for row in results:
|
||||
moment_info = {}
|
||||
moment_info["pk"] = row[0]
|
||||
moment_info["timezoneOffset"] = row[1]
|
||||
moment_info["trashedState"] = row[2]
|
||||
moment_info["approximateLatitude"] = row[3]
|
||||
moment_info["approximateLongitude"] = row[4]
|
||||
moment_info["endDate"] = row[5]
|
||||
moment_info["modificationDate"] = row[6]
|
||||
moment_info["representativeDate"] = row[7]
|
||||
moment_info["startDate"] = row[8]
|
||||
moment_info["subtitle"] = row[9]
|
||||
moment_info["title"] = row[10]
|
||||
moment_info["uuid"] = row[11]
|
||||
|
||||
# if both lat/lon == -180, then it means location undefined
|
||||
if (
|
||||
moment_info["approximateLatitude"] == -180.0
|
||||
and moment_info["approximateLongitude"] == -180.0
|
||||
):
|
||||
moment_info["latitude"] = None
|
||||
moment_info["longitude"] = None
|
||||
else:
|
||||
moment_info["latitude"] = moment_info["approximateLatitude"]
|
||||
moment_info["longitude"] = moment_info["approximateLongitude"]
|
||||
|
||||
# process date stamps
|
||||
offset_seconds = moment_info["timezoneOffset"] or 0
|
||||
delta = timedelta(seconds=offset_seconds)
|
||||
tz = timezone(delta)
|
||||
for date_name in [
|
||||
"startDate",
|
||||
"endDate",
|
||||
"modificationDate",
|
||||
"representativeDate",
|
||||
]:
|
||||
date_stamp = moment_info[date_name]
|
||||
try:
|
||||
moment_date = datetime.fromtimestamp(date_stamp + TIME_DELTA)
|
||||
# save raw time stamp valu
|
||||
moment_info[date_name + "_timestamp"] = moment_info[date_name]
|
||||
moment_info[date_name] = moment_date.astimezone(tz=tz)
|
||||
except ValueError:
|
||||
# sometimes imageDate is invalid so use 1 Jan 1970 in UTC as image date
|
||||
moment_date = datetime(1970, 1, 1)
|
||||
tz = timezone(timedelta(0))
|
||||
moment_info[date_name + "_timestamp"] = date_stamp
|
||||
moment_info[date_name] = moment_date.astimezone(tz=tz)
|
||||
|
||||
# process title/subtitle
|
||||
moment_info["title"] = moment_info["title"] or ""
|
||||
moment_info["subtitle"] = moment_info["subtitle"] or ""
|
||||
|
||||
self._db_moment_pk[moment_info["pk"]] = moment_info
|
||||
|
||||
def _build_album_folder_hierarchy_5(self, uuid, folders=None):
|
||||
"""recursively build folder/album hierarchy
|
||||
uuid: uuid of the album/folder being processed
|
||||
@@ -2702,7 +2840,7 @@ class PhotosDB:
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
return hierarchy
|
||||
|
||||
def _get_album_uuids(self, shared=False, import_session=False):
|
||||
def _get_album_uuids(self, shared=False, import_session=False, project=False):
|
||||
"""Return list of album UUIDs found in photos database
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
@@ -2710,20 +2848,21 @@ class PhotosDB:
|
||||
Args:
|
||||
shared: boolean; if True, returns shared albums, else normal albums
|
||||
import_session: boolean, if True, returns import session albums, else normal or shared albums
|
||||
project: boolean, if True, returns albums that are part of My Projects
|
||||
Note: flags (shared, import_session) are mutually exclusive
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: raised if mutually exclusive flags passed
|
||||
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if shared and import_session:
|
||||
if sum(bool(x) for x in [shared, import_session, project]) > 1:
|
||||
raise ValueError(
|
||||
"flags are mutually exclusive: pass zero or one of shared, import_session"
|
||||
"flags are mutually exclusive: pass zero or one of shared, import_session, projects"
|
||||
)
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
version4 = True
|
||||
if shared:
|
||||
logging.warning(
|
||||
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||
@@ -2734,16 +2873,44 @@ class PhotosDB:
|
||||
f"Import sessions not implemented for Photos library version {self._db_version}"
|
||||
)
|
||||
return [] # not implemented for _PHOTOS_4_VERSION
|
||||
else:
|
||||
elif project:
|
||||
album_type = [
|
||||
_PHOTOS_4_ALBUM_TYPE_PROJECT,
|
||||
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
|
||||
]
|
||||
album_kind = _PHOTOS_4_ALBUM_KIND
|
||||
else:
|
||||
version4 = False
|
||||
if shared:
|
||||
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
|
||||
elif import_session:
|
||||
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
|
||||
else:
|
||||
album_kind = _PHOTOS_5_ALBUM_KIND
|
||||
album_type = [_PHOTOS_4_ALBUM_TYPE_ALBUM]
|
||||
album_kind = _PHOTOS_4_ALBUM_KIND
|
||||
|
||||
album_list = []
|
||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||
for album, detail in self._dbalbum_details.items():
|
||||
if (
|
||||
detail["kind"] == album_kind
|
||||
and detail["albumType"] in album_type
|
||||
and not detail["intrash"]
|
||||
and (
|
||||
(shared and detail["cloudownerhashedpersonid"] is not None)
|
||||
or (not shared and detail["cloudownerhashedpersonid"] is None)
|
||||
)
|
||||
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
|
||||
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||
# but should not be listed here; they can be distinguished by looking
|
||||
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
):
|
||||
album_list.append(album)
|
||||
return album_list
|
||||
|
||||
# Photos version 5+
|
||||
if shared:
|
||||
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
|
||||
elif import_session:
|
||||
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
|
||||
elif project:
|
||||
album_kind = _PHOTOS_5_PROJECT_ALBUM_KIND
|
||||
else:
|
||||
album_kind = _PHOTOS_5_ALBUM_KIND
|
||||
|
||||
album_list = []
|
||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||
@@ -2755,13 +2922,6 @@ class PhotosDB:
|
||||
(shared and detail["cloudownerhashedpersonid"] is not None)
|
||||
or (not shared and detail["cloudownerhashedpersonid"] is None)
|
||||
)
|
||||
and (
|
||||
not version4
|
||||
# in Photos 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||
# but should not be listed here; they can be distinguished by looking
|
||||
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
|
||||
)
|
||||
):
|
||||
album_list.append(album)
|
||||
return album_list
|
||||
@@ -3286,9 +3446,9 @@ class PhotosDB:
|
||||
if options.regex:
|
||||
flags = re.IGNORECASE if options.ignore_case else 0
|
||||
render_options = RenderOptions(none_str="")
|
||||
photo_list = []
|
||||
for regex, template in options.regex:
|
||||
regex = re.compile(regex, flags)
|
||||
photo_list = []
|
||||
for p in photos:
|
||||
rendered, _ = p.render_template(template, render_options)
|
||||
for value in rendered:
|
||||
@@ -3348,12 +3508,45 @@ class PhotosDB:
|
||||
# selection only works if photos selected in main media browser
|
||||
photos = []
|
||||
|
||||
if options.exif:
|
||||
matching_photos = []
|
||||
for p in photos:
|
||||
if not p.exiftool:
|
||||
continue
|
||||
exifdata = p.exiftool.asdict(normalized=True)
|
||||
exifdata.update(p.exiftool.asdict(tag_groups=False, normalized=True))
|
||||
for exiftag, exifvalue in options.exif:
|
||||
if options.ignore_case:
|
||||
exifvalue = exifvalue.lower()
|
||||
exifdata_value = exifdata.get(exiftag.lower(), "")
|
||||
if isinstance(exifdata_value, str):
|
||||
exifdata_value = exifdata_value.lower()
|
||||
elif isinstance(exifdata_value, Iterable):
|
||||
exifdata_value = [v.lower() for v in exifdata_value]
|
||||
else:
|
||||
exifdata_value = str(exifdata_value)
|
||||
|
||||
if exifvalue in exifdata_value:
|
||||
matching_photos.append(p)
|
||||
else:
|
||||
exifdata_value = exifdata.get(exiftag.lower(), "")
|
||||
if not isinstance(exifdata_value, (str, Iterable)):
|
||||
exifdata_value = str(exifdata_value)
|
||||
if exifvalue in exifdata_value:
|
||||
matching_photos.append(p)
|
||||
photos = matching_photos
|
||||
|
||||
if options.function:
|
||||
for function in options.function:
|
||||
photos = function[0](photos)
|
||||
|
||||
return photos
|
||||
|
||||
def execute(self, sql):
|
||||
"""Execute sql statement and return cursor"""
|
||||
self._db_connection, _ = self.get_db_connection()
|
||||
return self._db_connection.cursor().execute(sql)
|
||||
|
||||
def _duplicate_signature(self, uuid):
|
||||
"""Compute a signature for finding possible duplicates"""
|
||||
return (
|
||||
@@ -3381,6 +3574,10 @@ class PhotosDB:
|
||||
"""
|
||||
return len(self._dbphotos)
|
||||
|
||||
def __del__(self):
|
||||
if getattr(self, "_db_connection", None):
|
||||
self._db_connection.close()
|
||||
|
||||
|
||||
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
|
||||
"""Search for photos based on values being in PhotoInfo.attribute
|
||||
|
||||
@@ -4,7 +4,11 @@ import logging
|
||||
import plistlib
|
||||
|
||||
from .._constants import (
|
||||
_PHOTOS_2_VERSION,
|
||||
_PHOTOS_3_VERSION,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_MODEL_VERSION,
|
||||
_PHOTOS_5_VERSION,
|
||||
_PHOTOS_6_MODEL_VERSION,
|
||||
_PHOTOS_7_MODEL_VERSION,
|
||||
_TESTED_DB_VERSIONS,
|
||||
@@ -83,3 +87,32 @@ def get_db_model_version(db_file):
|
||||
logging.warning(f"Unknown model version: {model_ver}")
|
||||
# cross our fingers and try latest version
|
||||
return 7
|
||||
|
||||
|
||||
class UnknownLibraryVersion(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_photos_library_version(library_path):
|
||||
"""Return int indicating which Photos version a library was created with """
|
||||
library_path = pathlib.Path(library_path)
|
||||
db_ver = get_db_version(str(library_path / "database" / "photos.db"))
|
||||
db_ver = int(db_ver)
|
||||
if db_ver == int(_PHOTOS_2_VERSION):
|
||||
return 2
|
||||
if db_ver == int(_PHOTOS_3_VERSION):
|
||||
return 3
|
||||
if db_ver == int(_PHOTOS_4_VERSION):
|
||||
return 4
|
||||
if db_ver != int(_PHOTOS_5_VERSION):
|
||||
raise UnknownLibraryVersion(f"db_ver = {db_ver}")
|
||||
|
||||
model_ver = get_model_version(str(library_path / "database" / "Photos.sqlite"))
|
||||
model_ver = int(model_ver)
|
||||
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
|
||||
return 5
|
||||
if _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
|
||||
return 6
|
||||
if _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
|
||||
return 7
|
||||
raise UnknownLibraryVersion(f"db_ver = {db_ver}, model_ver = {model_ver}")
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 json
|
||||
@@ -27,7 +27,7 @@ from .utils import expand_and_validate_filepath, load_function
|
||||
# ensure locale set to user's locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
OTL_GRAMMAR_MODEL = str(pathlib.Path(__file__).parent / "phototemplate.tx")
|
||||
MTL_GRAMMAR_MODEL = str(pathlib.Path(__file__).parent / "phototemplate.tx")
|
||||
|
||||
"""TextX metamodel for osxphotos template language """
|
||||
|
||||
@@ -181,6 +181,9 @@ TEMPLATE_SUBSTITUTIONS_PATHLIB = {
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{album}": "Album(s) photo is contained in",
|
||||
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{project}": "Project(s) photo is contained in (such as greeting cards, calendars, slideshows)",
|
||||
"{album_project}": "Album(s) and project(s) photo is contained in; treats projects as regular albums",
|
||||
"{folder_album_project}": "Folder path + album (includes projects as albums) photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{keyword}": "Keyword(s) assigned to photo",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
||||
@@ -324,7 +327,7 @@ class PhotoTemplateParser:
|
||||
if hasattr(self, "metamodel"):
|
||||
return
|
||||
|
||||
self.metamodel = metamodel_from_file(OTL_GRAMMAR_MODEL, skipws=False)
|
||||
self.metamodel = metamodel_from_file(MTL_GRAMMAR_MODEL, skipws=False)
|
||||
|
||||
def parse(self, template_statement):
|
||||
"""Parse a template_statement string"""
|
||||
@@ -1004,6 +1007,9 @@ class PhotoTemplate:
|
||||
elif self.dirname:
|
||||
value = sanitize_dirname(value)
|
||||
|
||||
# ensure no empty strings in value (see #512)
|
||||
value = None if value == "" else value
|
||||
|
||||
return [value]
|
||||
|
||||
def get_template_value_pathlib(self, field):
|
||||
@@ -1113,6 +1119,11 @@ class PhotoTemplate:
|
||||
values = []
|
||||
if field == "album":
|
||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
||||
elif field == "project":
|
||||
values = [p.title for p in self.photo.project_info]
|
||||
elif field == "album_project":
|
||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
||||
values += [p.title for p in self.photo.project_info]
|
||||
elif field == "keyword":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -1123,13 +1134,15 @@ class PhotoTemplate:
|
||||
values = self.photo.labels
|
||||
elif field == "label_normalized":
|
||||
values = self.photo.labels_normalized
|
||||
elif field == "folder_album":
|
||||
elif field in ["folder_album", "folder_album_project"]:
|
||||
values = []
|
||||
# photos must be in an album to be in a folder
|
||||
if self.photo.burst:
|
||||
album_info = self.photo.burst_album_info
|
||||
else:
|
||||
album_info = self.photo.album_info
|
||||
if field == "folder_album_project":
|
||||
album_info += self.photo.project_info
|
||||
for album in album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
@@ -1190,7 +1203,7 @@ class PhotoTemplate:
|
||||
elif isinstance(obj, (str, int, float)):
|
||||
values = [str(obj)]
|
||||
else:
|
||||
values = [val for val in obj]
|
||||
values = list(obj)
|
||||
elif field == "detected_text":
|
||||
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
|
||||
else:
|
||||
@@ -1199,7 +1212,7 @@ class PhotoTemplate:
|
||||
# sanitize directory names if needed, folder_album handled differently above
|
||||
if self.filename:
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif self.dirname and field != "folder_album":
|
||||
elif self.dirname and field not in ["folder_album", "folder_album_project"]:
|
||||
# skip folder_album because it would have been handled above
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// OSXPhotos Template Language (OTL)
|
||||
// OSXPhotos Metadata Template Language (MTL)
|
||||
// a TemplateString has format:
|
||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
|
||||
// a TemplateStatement may contain zero or more TemplateStrings
|
||||
|
||||
131
osxphotos/pyrepl.py
Normal file
@@ -0,0 +1,131 @@
|
||||
""" Custom Python REPL based on ptpython that allows quitting with custom keywords instead of `quit()` """
|
||||
|
||||
""" This file is distributed under the same license as the ptpython package:
|
||||
|
||||
Copyright (c) 2015, Jonathan Slenders (ptpython), (c) 2021 Rhet Turnbull (this file)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the {organization} nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from ptpython.repl import (
|
||||
ContextManager,
|
||||
DummyContext,
|
||||
PythonRepl,
|
||||
builtins,
|
||||
patch_stdout_context,
|
||||
)
|
||||
|
||||
|
||||
class PyReplQuitter(PythonRepl):
|
||||
"""Custom pypython repl that allows quitting REPL with custom commands"""
|
||||
|
||||
def __init__(self, *args, quit_words: Optional[List[str]] = None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.quit_words = quit_words or ["quit", "q"]
|
||||
|
||||
def eval(self, line: str) -> object:
|
||||
if line.strip() in self.quit_words:
|
||||
sys.exit(0)
|
||||
return super().eval(line)
|
||||
|
||||
|
||||
def embed_repl(
|
||||
globals=None,
|
||||
locals=None,
|
||||
configure: Optional[Callable[[PythonRepl], None]] = None,
|
||||
vi_mode: bool = False,
|
||||
history_filename: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
startup_paths=None,
|
||||
patch_stdout: bool = False,
|
||||
return_asyncio_coroutine: bool = False,
|
||||
quit_words: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Call this to embed Python shell at the current point in your program.
|
||||
It's similar to `IPython.embed` and `bpython.embed`. ::
|
||||
from prompt_toolkit.contrib.repl import embed
|
||||
embed(globals(), locals())
|
||||
:param vi_mode: Boolean. Use Vi instead of Emacs key bindings.
|
||||
:param configure: Callable that will be called with the `PythonRepl` as a first
|
||||
argument, to trigger configuration.
|
||||
:param title: Title to be displayed in the terminal titlebar. (None or string.)
|
||||
:param patch_stdout: When true, patch `sys.stdout` so that background
|
||||
threads that are printing will print nicely above the prompt.
|
||||
"""
|
||||
# Default globals/locals
|
||||
if globals is None:
|
||||
globals = {
|
||||
"__name__": "__main__",
|
||||
"__package__": None,
|
||||
"__doc__": None,
|
||||
"__builtins__": builtins,
|
||||
}
|
||||
|
||||
locals = locals or globals
|
||||
|
||||
def get_globals():
|
||||
return globals
|
||||
|
||||
def get_locals():
|
||||
return locals
|
||||
|
||||
# Create REPL.
|
||||
repl = PyReplQuitter(
|
||||
get_globals=get_globals,
|
||||
get_locals=get_locals,
|
||||
vi_mode=vi_mode,
|
||||
history_filename=history_filename,
|
||||
startup_paths=startup_paths,
|
||||
quit_words=quit_words,
|
||||
)
|
||||
|
||||
if title:
|
||||
repl.terminal_title = title
|
||||
|
||||
if configure:
|
||||
configure(repl)
|
||||
|
||||
# Start repl.
|
||||
patch_context: ContextManager = (
|
||||
patch_stdout_context() if patch_stdout else DummyContext()
|
||||
)
|
||||
|
||||
if return_asyncio_coroutine:
|
||||
|
||||
async def coroutine():
|
||||
with patch_context:
|
||||
await repl.run_async()
|
||||
|
||||
return coroutine()
|
||||
else:
|
||||
with patch_context:
|
||||
repl.run()
|
||||
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}"
|
||||
36
osxphotos/query_builder.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Build sql queries from template to retrieve info from the database"""
|
||||
|
||||
import os.path
|
||||
import pathlib
|
||||
from functools import lru_cache
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
from ._constants import _DB_TABLE_NAMES
|
||||
|
||||
QUERY_DIR = os.path.join(os.path.dirname(__file__), "queries")
|
||||
|
||||
|
||||
def get_query(query_name, photos_ver, **kwargs):
|
||||
"""Return sqlite query string for an attribute and a given database version"""
|
||||
|
||||
# there can be a single query for multiple database versions or separate queries for each version
|
||||
# try generic version first (most common case), if that fails, look for version specific query
|
||||
query_string = _get_query_string(query_name, photos_ver)
|
||||
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||
query_template = Template(query_string)
|
||||
return query_template.render(asset_table=asset_table, **kwargs)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _get_query_string(query_name, photos_ver):
|
||||
"""Return sqlite query string for an attribute and a given database version"""
|
||||
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}.sql.mako"
|
||||
if not query_file.is_file():
|
||||
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}_{photos_ver}.sql.mako"
|
||||
if not query_file.is_file():
|
||||
raise FileNotFoundError(f"Query file '{query_file}' not found")
|
||||
|
||||
with open(query_file, "r") as f:
|
||||
query_string = f.read()
|
||||
return query_string
|
||||
@@ -84,6 +84,7 @@ class QueryOptions:
|
||||
no_location: Optional[bool] = None
|
||||
function: Optional[List[Tuple[callable, str]]] = None
|
||||
selected: Optional[bool] = None
|
||||
exif: Optional[Iterable[Tuple[str, str]]] = None
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import objc
|
||||
import Quartz
|
||||
@@ -22,8 +22,13 @@ else:
|
||||
vision = True
|
||||
|
||||
|
||||
def detect_text(img_path: str) -> List:
|
||||
"""process image at img_path with VNRecognizeTextRequest and return list of results"""
|
||||
def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
|
||||
"""process image at img_path with VNRecognizeTextRequest and return list of results
|
||||
|
||||
Args:
|
||||
img_path: path to the image file
|
||||
orientation: optional EXIF orientation (if known, passing orientation may improve quality of results)
|
||||
"""
|
||||
if not vision:
|
||||
logging.warning(f"detect_text not implemented for this version of macOS")
|
||||
return []
|
||||
@@ -40,9 +45,18 @@ def detect_text(img_path: str) -> List:
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
vision_options = NSDictionary.dictionaryWithDictionary_({})
|
||||
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
|
||||
input_image, vision_options
|
||||
)
|
||||
if orientation is not None:
|
||||
if not 1 <= orientation <= 8:
|
||||
raise ValueError("orientation must be between 1 and 8")
|
||||
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
|
||||
input_image, orientation, vision_options
|
||||
)
|
||||
else:
|
||||
vision_handler = (
|
||||
Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
|
||||
input_image, vision_options
|
||||
)
|
||||
)
|
||||
results = []
|
||||
handler = make_request_handler(results)
|
||||
vision_request = (
|
||||
|
||||
@@ -528,35 +528,38 @@ def _get_uti_from_mdls(extension):
|
||||
# mdls -name kMDItemContentType foo.3fr
|
||||
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"/usr/bin/mdls",
|
||||
"-name",
|
||||
"kMDItemContentType",
|
||||
temp.name,
|
||||
]
|
||||
).splitlines()
|
||||
output = output[0].decode("UTF-8") if output else None
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"/usr/bin/mdls",
|
||||
"-name",
|
||||
"kMDItemContentType",
|
||||
temp.name,
|
||||
]
|
||||
).splitlines()
|
||||
output = output[0].decode("UTF-8") if output else None
|
||||
if not output:
|
||||
return None
|
||||
|
||||
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_uti_from_ext_dict(ext):
|
||||
try:
|
||||
return EXT_UTI_DICT[ext]
|
||||
return EXT_UTI_DICT[ext.lower()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def _get_ext_from_uti_dict(uti):
|
||||
try:
|
||||
return UTI_EXT_DICT[uti]
|
||||
return UTI_EXT_DICT[uti.lower()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ import sys
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
from plistlib import load as plistload
|
||||
from typing import Callable
|
||||
from typing import Callable, Union
|
||||
|
||||
import CoreFoundation
|
||||
import objc
|
||||
from Foundation import NSString
|
||||
|
||||
from ._constants import UNICODE_FORMAT
|
||||
|
||||
@@ -263,6 +265,13 @@ def list_photo_libraries():
|
||||
return lib_list
|
||||
|
||||
|
||||
def normalize_fs_path(path: str) -> str:
|
||||
"""Normalize filesystem paths with unicode in them"""
|
||||
with objc.autorelease_pool():
|
||||
normalized_path = NSString.fileSystemRepresentation(path)
|
||||
return normalized_path.decode("utf8")
|
||||
|
||||
|
||||
def findfiles(pattern, path_):
|
||||
"""Returns list of filenames from path_ matched by pattern
|
||||
shell pattern. Matching is case-insensitive.
|
||||
@@ -271,8 +280,11 @@ def findfiles(pattern, path_):
|
||||
return []
|
||||
# See: https://gist.github.com/techtonik/5694830
|
||||
|
||||
# paths need to be normalized for unicode as filesystem returns unicode in NFD form
|
||||
pattern = normalize_fs_path(pattern)
|
||||
rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
|
||||
return [name for name in os.listdir(path_) if rule.match(name)]
|
||||
files = [normalize_fs_path(p) for p in os.listdir(path_)]
|
||||
return [name for name in files if rule.match(name)]
|
||||
|
||||
|
||||
def _open_sql_file(dbname):
|
||||
@@ -353,30 +365,50 @@ def normalize_unicode(value):
|
||||
return None
|
||||
|
||||
|
||||
def increment_filename(filepath):
|
||||
def increment_filename_with_count(filepath: Union[str,pathlib.Path], count: int = 0) -> str:
|
||||
"""Return filename (1).ext, etc if filename.ext exists
|
||||
|
||||
If file exists in filename's parent folder with same stem as filename,
|
||||
add (1), (2), etc. until a non-existing filename is found.
|
||||
|
||||
Args:
|
||||
filepath: str; full path, including file name
|
||||
filepath: str or pathlib.Path; full path, including file name
|
||||
count: int; starting increment value
|
||||
|
||||
Returns:
|
||||
tuple of new filepath (or same if not incremented), count
|
||||
|
||||
Note: This obviously is subject to race condition so using with caution.
|
||||
"""
|
||||
dest = filepath if isinstance(filepath, pathlib.Path) else pathlib.Path(filepath)
|
||||
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
||||
dest_files = [normalize_fs_path(pathlib.Path(f).stem.lower()) for f in dest_files]
|
||||
dest_new = dest.stem
|
||||
if count:
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
while normalize_fs_path(dest_new.lower()) in dest_files:
|
||||
count += 1
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
return str(dest), count
|
||||
|
||||
|
||||
def increment_filename(filepath: Union[str, pathlib.Path]) -> str:
|
||||
"""Return filename (1).ext, etc if filename.ext exists
|
||||
|
||||
If file exists in filename's parent folder with same stem as filename,
|
||||
add (1), (2), etc. until a non-existing filename is found.
|
||||
|
||||
Args:
|
||||
filepath: str or pathlib.Path; full path, including file name
|
||||
|
||||
Returns:
|
||||
new filepath (or same if not incremented)
|
||||
|
||||
Note: This obviously is subject to race condition so using with caution.
|
||||
"""
|
||||
dest = pathlib.Path(str(filepath))
|
||||
count = 1
|
||||
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||
dest_new = dest.stem
|
||||
while dest_new.lower() in dest_files:
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
count += 1
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
return str(dest)
|
||||
new_filepath, _ = increment_filename_with_count(filepath)
|
||||
return new_filepath
|
||||
|
||||
|
||||
def expand_and_validate_filepath(path: str) -> str:
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
pyobjc-core>=7.2
|
||||
pyobjc-framework-AppleScriptKit>=7.2
|
||||
pyobjc-framework-AppleScriptObjC>=7.2
|
||||
pyobjc-framework-Photos>=7.2
|
||||
pyobjc-framework-Quartz>=7.2
|
||||
pyobjc-framework-AVFoundation>=7.2
|
||||
pyobjc-framework-CoreServices>=7.2
|
||||
pyobjc-framework-Metal>=7.2
|
||||
pyobjc-framework-Vision>=7.2
|
||||
Click==8.0.1
|
||||
PyYAML==5.4.1
|
||||
Mako==1.1.4
|
||||
Click>=8.0.1,<9.0
|
||||
Mako>=1.1.4,<1.2.0
|
||||
PyYAML>=5.4.1,<6.0.0
|
||||
bitmath>=1.3.3.1,<1.4.0.0
|
||||
bpylist2==3.0.2
|
||||
pathvalidate==2.4.1
|
||||
dataclasses==0.7;python_version<'3.7'
|
||||
wurlitzer==2.1.0
|
||||
photoscript==0.1.4
|
||||
toml==0.10.2
|
||||
osxmetadata==0.99.31
|
||||
textx==2.3.0
|
||||
rich==10.6.0
|
||||
bitmath==1.3.3.1
|
||||
more-itertools==8.8.0
|
||||
more-itertools>=8.8.0,<9.0.0
|
||||
objexplore>=1.5.5,<1.6.0
|
||||
osxmetadata>=0.99.34,<1.0.0
|
||||
pathvalidate>=2.4.1,<2.5.0
|
||||
photoscript>=0.1.4,<0.2.0
|
||||
ptpython>=3.0.20,<3.1.0
|
||||
pyobjc-core>=7.3,<9.0
|
||||
pyobjc-framework-AVFoundation>=7.3,<9.0
|
||||
pyobjc-framework-AppleScriptKit>=7.3,<9.0
|
||||
pyobjc-framework-AppleScriptObjC>=7.3,<9.0
|
||||
pyobjc-framework-Cocoa>=7.3,<9.0
|
||||
pyobjc-framework-CoreServices>=7.2,<9.0
|
||||
pyobjc-framework-Metal>=7.3,<9.0
|
||||
pyobjc-framework-Photos>=7.3,<9.0
|
||||
pyobjc-framework-Quartz>=7.3,<9.0
|
||||
pyobjc-framework-Vision>=7.3,<9.0
|
||||
rich>=10.6.0,<=11.0.0
|
||||
textx>=2.3.0,<2.4.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
wurlitzer>=2.1.0,<2.2.0
|
||||
@@ -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.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
install_requires=[
|
||||
"pyobjc-core",
|
||||
"pyobjc-framework-AppleScriptKit",
|
||||
"pyobjc-framework-AppleScriptObjC",
|
||||
"pyobjc-framework-Photos",
|
||||
"pyobjc-framework-Quartz",
|
||||
"pyobjc-framework-AVFoundation",
|
||||
"pyobjc-framework-CoreServices",
|
||||
"pyobjc-framework-Metal",
|
||||
"pyobjc-framework-Vision",
|
||||
"Click==8.0.1",
|
||||
"PyYAML==5.4.1",
|
||||
"Mako==1.1.4",
|
||||
"Click>=8.0.1,<9.0",
|
||||
"Mako>=1.1.4,<1.2.0",
|
||||
"PyYAML>=5.4.1,<5.5.0",
|
||||
"bitmath>=1.3.3.1,<1.4.0.0",
|
||||
"bpylist2==3.0.2",
|
||||
"pathvalidate==2.4.1",
|
||||
"dataclasses==0.7;python_version<'3.7'",
|
||||
"wurlitzer==2.1.0",
|
||||
"photoscript==0.1.4",
|
||||
"toml==0.10.2",
|
||||
"osxmetadata==0.99.31",
|
||||
"textx==2.3.0",
|
||||
"rich==10.6.0",
|
||||
"bitmath==1.3.3.1",
|
||||
"more-itertools==8.8.0",
|
||||
"more-itertools>=8.8.0,<9.0.0",
|
||||
"objexplore>=1.5.5,<1.6.0",
|
||||
"osxmetadata>=0.99.34,<1.0.0",
|
||||
"pathvalidate>=2.4.1,<3.0.0",
|
||||
"photoscript>=0.1.4,<0.2.0",
|
||||
"ptpython>=3.0.20,<4.0.0",
|
||||
"pyobjc-core>=7.3,<9.0",
|
||||
"pyobjc-framework-AVFoundation>=7.3,<9.0",
|
||||
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
|
||||
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
|
||||
"pyobjc-framework-Cocoa>=7.3,<9.0",
|
||||
"pyobjc-framework-CoreServices>=7.2,<9.0",
|
||||
"pyobjc-framework-Metal>=7.3,<9.0",
|
||||
"pyobjc-framework-Photos>=7.3,<9.0",
|
||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||
"rich>=10.6.0,<=11.0.0",
|
||||
"textx>=2.3.0,<3.0.0",
|
||||
"toml>=0.10.2,<0.11.0",
|
||||
"wurlitzer>=2.1.0,<3.0.0",
|
||||
],
|
||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||
include_package_data=True,
|
||||
|
||||
|
After Width: | Height: | Size: 10 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 10 MiB |
|
After Width: | Height: | Size: 75 KiB |
@@ -3,24 +3,24 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BackgroundHighlightCollection</key>
|
||||
<date>2021-07-20T05:48:01Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
<key>BackgroundHighlightEnrichment</key>
|
||||
<date>2021-07-20T05:48:00Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
<key>BackgroundJobAssetRevGeocode</key>
|
||||
<date>2021-07-20T07:05:31Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
<key>BackgroundJobSearch</key>
|
||||
<date>2021-07-20T05:48:01Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
<key>BackgroundPeopleSuggestion</key>
|
||||
<date>2021-07-20T05:48:00Z</date>
|
||||
<date>2021-09-14T04:40:41Z</date>
|
||||
<key>BackgroundUserBehaviorProcessor</key>
|
||||
<date>2021-07-20T05:48:01Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||
<date>2021-07-20T05:48:08Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2021-07-20T05:47:59Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2021-07-20T05:48:01Z</date>
|
||||
<date>2021-09-14T04:40:43Z</date>
|
||||
<key>SiriPortraitDonation</key>
|
||||
<date>2021-07-20T05:48:01Z</date>
|
||||
<date>2021-09-14T04:40:42Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FaceIDModelLastGenerationKey</key>
|
||||
<date>2021-07-20T05:48:02Z</date>
|
||||
<date>2021-09-14T04:49:52Z</date>
|
||||
<key>LastContactClassificationKey</key>
|
||||
<date>2021-07-20T05:48:05Z</date>
|
||||
<date>2021-09-14T04:51:05Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PVClustererBringUpState</key>
|
||||
<integer>50</integer>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 56 KiB |