Compare commits

...

465 Commits

Author SHA1 Message Date
Rhet Turnbull
5a8105f5a0 Fix for #575, database version 5001 2022-01-09 07:44:38 -08:00
allcontributors[bot]
df66adeef6 docs: add ahti123 as a contributor for code, bug (#578)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-09 07:29:15 -08:00
Ahti Liin
4e2367c868 changing photos_5 version constant to satisfy version 5001 (#577)
Co-authored-by: Ahti Liin <ahti@mooncascade.com>
2022-01-09 07:28:22 -08:00
Rhet Turnbull
53c701cc0e Added sqlgrep 2022-01-08 17:41:06 -08:00
Rhet Turnbull
92fced75da Added test for #576 2022-01-08 17:39:49 -08:00
Rhet Turnbull
4dd838b8bc Added grep command to CLI 2022-01-08 17:14:36 -08:00
Rhet Turnbull
0a3c375943 Updated CHANGELOG.md [skip ci] 2022-01-08 15:23:41 -08:00
Rhet Turnbull
64a0760a47 Updated docs [skip ci] 2022-01-08 15:23:14 -08:00
Rhet Turnbull
2e7db47806 Fix for #576, error exporting edited live photos 2022-01-08 15:15:28 -08:00
Rhet Turnbull
d2d56a7f71 Fix for burst images with pick type = 0, partial fix for #571 2022-01-06 22:46:16 -08:00
Rhet Turnbull
b4897ff1b5 version bump [skip ci] 2022-01-06 22:16:12 -08:00
Rhet Turnbull
661a573bf5 Fix for #570 2022-01-06 22:13:25 -08:00
Rhet Turnbull
0c9bd87602 More refactoring of export code, #462 2022-01-06 05:40:47 -08:00
Rhet Turnbull
896d888710 Updated CHANGELOG.md [skip ci] 2022-01-04 06:35:23 -08:00
Rhet Turnbull
76aee7f189 Export DB can now reside outside export directory, #568 2022-01-04 06:28:59 -08:00
Rhet Turnbull
147b30f973 More refactoring of export code, #462 2022-01-02 22:38:22 -08:00
Rhet Turnbull
a73dc72558 Refactored photoinfo, photoexporter; #462 2022-01-02 09:06:04 -08:00
Rhet Turnbull
c99cf5518d Updated CHANGELOG.md [skip ci] 2021-12-31 20:50:42 -08:00
Rhet Turnbull
1391675a3a Updated tests and docs 2021-12-31 20:31:15 -08:00
Rhet Turnbull
a3b2784f31 ImageConverter now uses generic context; #562 2021-12-31 17:34:41 -08:00
Rhet Turnbull
cbe79ee98c Updated docs [skip ci] 2021-12-31 09:45:42 -08:00
Rhet Turnbull
eb7a2988bf Updated CHANGELOG.md [skip ci] 2021-12-31 09:45:19 -08:00
Rhet Turnbull
42426b95ee Bug fix for #559 2021-12-31 09:35:12 -08:00
Rhet Turnbull
262a6f31e7 Updated CHANGELOG.md [skip ci] 2021-12-31 08:39:18 -08:00
Rhet Turnbull
04930c3644 Added --skip-uuid, --skip-uuid-from-file, #563 2021-12-31 08:35:26 -08:00
Rhet Turnbull
44594a8e43 Added support for projects, implements #559 2021-12-31 07:30:20 -08:00
Rhet Turnbull
690d981f31 Fixed test for #561 2021-12-30 15:45:32 -08:00
Rhet Turnbull
06ea8d1e6c Updated CHANGELOG.md [skip ci] 2021-12-29 11:24:29 -08:00
Rhet Turnbull
c4e3c5a8be Updated docs [skip ci] 2021-12-28 17:37:00 -08:00
Rhet Turnbull
03f4e7cc34 Fix for accented characters in album names, #561 2021-12-28 17:25:50 -08:00
Rhet Turnbull
0e54a08ae0 Updated docs [skip ci] 2021-12-26 20:09:10 -08:00
Rhet Turnbull
b71c752e9d Added get_photos_library_version 2021-12-26 19:57:33 -08:00
Rhet Turnbull
521848f955 Added export test for --exif 2021-12-25 05:53:26 -08:00
Rhet Turnbull
debb17c952 Implement #323 2021-12-25 05:41:37 -08:00
Rhet Turnbull
7819740f70 Fixed #463 2021-12-24 18:12:02 -08:00
Rhet Turnbull
b9ffb0d8de Fixed helped text, #493 2021-12-24 18:08:18 -08:00
Rhet Turnbull
d59852f594 Updated tests 2021-12-24 17:21:13 -08:00
Rhet Turnbull
085f482820 Added install/uninstall commands, #531 2021-12-24 17:05:01 -08:00
Rhet Turnbull
1cb8da96ce Updated dev_requirements.txt 2021-12-22 19:08:25 -08:00
Rhet Turnbull
50016a9eca Updated requirements_dev.txt 2021-12-22 18:10:15 -08:00
Rhet Turnbull
924f7325b4 Removed redundant dev_requirements.txt 2021-12-22 18:08:44 -08:00
Rhet Turnbull
181f678d9e Updated docs [skip ci] 2021-12-22 08:22:02 -08:00
Rhet Turnbull
6ce1b83ca2 Version bump 2021-12-21 09:39:05 -08:00
Rhet Turnbull
a08a653f20 Partial fix for #556 2021-12-21 09:36:42 -08:00
Rhet Turnbull
e1f1772080 Updated all-contributors 2021-12-16 22:11:52 -08:00
Andrew Louis
d2a1f792e9 Adds missing f-string to retry message (#553) 2021-12-16 17:23:33 -08:00
Rhet Turnbull
e7bd80e05f Update issue templates 2021-12-11 22:24:38 -08:00
Rhet Turnbull
9089c0323c Updated CHANGELOG.md [skip ci] 2021-12-10 20:40:40 -08:00
Rhet Turnbull
197e5663df Updated docs 2021-12-10 20:26:00 -08:00
Rhet Turnbull
f6dedaa619 Updated docs [skip ci] 2021-12-10 20:09:20 -08:00
Rhet Turnbull
0906dbe637 Fixed error for missing photo path, #547 2021-12-10 19:36:25 -08:00
Rhet Turnbull
0629b3f6d6 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-12-10 19:26:29 -08:00
Rhet Turnbull
55dfc0ec7d Fixed typo, thanks to @hyfen 2021-12-10 19:26:21 -08:00
Andrew Louis
b7f8b26f1d Fixes typo in README (#548) 2021-12-10 19:23:56 -08:00
Rhet Turnbull
9ca7dd50bc Updated all-contributors 2021-12-10 19:23:12 -08:00
allcontributors[bot]
0e73d57bdf docs: add alandefreitas as a contributor for bug (#551)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-12-10 19:08:32 -08:00
Rhet Turnbull
9de2c17e47 Added additional try/except in _photoinfo_export.py 2021-12-06 06:16:42 -08:00
Rhet Turnbull
1bae6d33f1 Updated to moment processing for consistency 2021-12-06 06:07:55 -08:00
Rhet Turnbull
a52b4d2f43 Added MomentInfo for Photos 5+, #71 2021-12-05 19:12:37 -08:00
Rhet Turnbull
3e038bf124 Added test library for Monterey on M1 2021-12-05 08:41:08 -08:00
Rhet Turnbull
870ed9c435 Updated README.md for Monterey 2021-12-04 10:46:06 -08:00
allcontributors[bot]
68e7ca3277 docs: add dgleich as a contributor for code (#541)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-11-25 10:06:35 -08:00
Rhet Turnbull
c9142c2156 Updated CHANGELOG.md [skip ci] 2021-11-25 10:01:54 -08:00
Rhet Turnbull
7d923590ae Updated dependencies for pyobjc 8.0 2021-11-25 08:34:55 -08:00
Rhet Turnbull
5383ced1ca Updated CHANGELOG.md [skip ci] 2021-11-11 11:05:19 -08:00
Rhet Turnbull
0e6c92dbd9 Fix for --use-photokit with --skip-live, #537 2021-11-11 10:54:48 -08:00
Rhet Turnbull
b00978c61a Updated CHANGELOG.md [skip ci] 2021-11-07 21:35:46 -08:00
Rhet Turnbull
fb583e28e0 Updated docs [skip ci] 2021-11-07 21:31:27 -08:00
Rhet Turnbull
760386e3d7 Updated tested versions 2021-11-07 21:21:50 -08:00
Rhet Turnbull
51ba54971a Test fixes for Monterey/M1 2021-11-07 08:33:08 -08:00
Rhet Turnbull
2ffcf1e82b Updated OTL to MTL 2021-11-06 07:13:02 -07:00
Rhet Turnbull
818f4f45a4 Dependency update for Monterey 2021-10-30 07:37:23 -07:00
Rhet Turnbull
2cf19f6af1 Updated docs [skip ci] 2021-10-30 07:25:26 -07:00
Rhet Turnbull
ef82c6e32b Updated for Monterey 12.0.1 release 2021-10-28 22:05:17 -07:00
Rhet Turnbull
0e9b9d6251 Updated docs [skip ci] 2021-10-15 05:45:17 -07:00
Rhet Turnbull
419b34ea73 Fix for #526 with --update 2021-10-15 05:36:41 -07:00
Rhet Turnbull
f64c4ed374 Fixed FileUtil to use correct import 2021-10-14 21:29:45 -07:00
Rhet Turnbull
1677f404d2 Updated CHANGELOG.md [skip ci] 2021-10-11 18:07:36 -07:00
allcontributors[bot]
a612a363ed docs: add spencerc99 as a contributor for bug (#527)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-11 18:03:50 -07:00
Rhet Turnbull
202bc1144b Fix for #526 2021-10-11 17:50:07 -07:00
Rhet Turnbull
a0c654e43f Updated README.md [skip ci] 2021-10-11 17:03:45 -07:00
Rhet Turnbull
2bb677dc19 Updated docs [skip ci] 2021-10-11 16:52:22 -07:00
Rhet Turnbull
e33805fe42 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-10-11 16:00:05 -07:00
Rhet Turnbull
04ac0a1121 Fix for #524 2021-10-11 15:59:40 -07:00
Rhet Turnbull
d2b0bd4e28 Fix for #525 2021-10-11 15:59:02 -07:00
allcontributors[bot]
d754899563 docs: add oPromessa as a contributor for bug (#525)
* docs: update README.md [skip ci]

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

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

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-08-29 07:07:51 -07:00
Rhet Turnbull
48bf326994 Updated CHANGELOG.md [skip ci] 2021-08-28 09:21:07 -07:00
Rhet Turnbull
159d1102aa Added {strip} template 2021-08-28 08:14:26 -07:00
Rhet Turnbull
dbb4dbc0a7 Fixed --strip behavior, #511 2021-08-28 08:01:08 -07:00
Rhet Turnbull
777e768243 Added selected and quit to repl 2021-08-28 07:23:17 -07:00
Rhet Turnbull
70999a70b8 Updated tutorial template 2021-08-27 23:52:14 -07:00
Rhet Turnbull
3a6b2c2c35 Update test_cli.py 2021-08-23 18:36:35 -07:00
Rhet Turnbull
dfb80ba8d6 Update test_cli.py 2021-08-23 18:30:34 -07:00
Rhet Turnbull
94b818b156 Update test_cli.py 2021-08-23 18:09:36 -07:00
Rhet Turnbull
f1cea1498b Update test for #506 2021-08-23 17:57:28 -07:00
Rhet Turnbull
345678577a Updated test for #506 2021-08-23 17:29:38 -07:00
Rhet Turnbull
fb4138cfe6 Updated README [skip ci] 2021-08-23 14:25:13 -07:00
Rhet Turnbull
db5b34d589 Fix for #506 2021-08-23 14:23:39 -07:00
Rhet Turnbull
8963af9229 Updated CHANGELOG.md [skip ci] 2021-08-15 14:14:51 -07:00
Rhet Turnbull
2041789ff4 Updated README.md [skip ci] 2021-08-15 14:12:15 -07:00
Rhet Turnbull
aec86f93ea Added inspect() to repl, closes #501 2021-08-15 13:50:37 -07:00
Rhet Turnbull
57bfb03e05 Updated CHANGELOG.md [skip ci] 2021-08-02 05:55:19 -07:00
Rhet Turnbull
c2b2476e38 Updated docs for Text Detection [skip ci] 2021-08-02 05:52:48 -07:00
Rhet Turnbull
fa2027d453 Improved caching of detected_text results 2021-08-02 05:10:26 -07:00
Rhet Turnbull
9d980e4917 Updated CHANGELOG.md [skip ci] 2021-07-29 21:27:51 -07:00
Rhet Turnbull
673243c6cd Fix for #500, check for macOS version before loading Vision 2021-07-29 21:16:33 -07:00
Rhet Turnbull
7376223eb8 Updated text_detection to detect macOS version 2021-07-29 07:39:01 -07:00
Rhet Turnbull
ecd0b8e22f Updated detected_text docs to make it clear this only works on Catalina+ 2021-07-29 07:03:04 -07:00
Rhet Turnbull
c4a608b5bd Updated CHANGELOG.md [skip ci] 2021-07-29 07:02:38 -07:00
Rhet Turnbull
4d9e21ea16 Added error logging to PhotoInfo.detected_text 2021-07-29 06:32:07 -07:00
Rhet Turnbull
1ee3e035c4 Updated README.md [skip ci] 2021-07-29 06:25:59 -07:00
Rhet Turnbull
b1c0fb3e82 Added error logging to {detected_text} processing, #499 2021-07-29 06:23:02 -07:00
Rhet Turnbull
de715d2afd Updated CHANGELOG.md [skip ci] 2021-07-28 06:36:10 -07:00
Rhet Turnbull
607cf80dda Removed unneeded test file [skip ci] 2021-07-28 06:27:44 -07:00
Rhet Turnbull
0c8fbd69af Updated dependencies 2021-07-28 06:21:21 -07:00
Rhet Turnbull
c2335236be Added {detected_text} template 2021-07-27 06:08:49 -07:00
Rhet Turnbull
123340eada Added PhotoInfo.detected_text() 2021-07-25 18:34:59 -07:00
Rhet Turnbull
852a06f99b Updated docs 2021-07-24 21:30:52 -07:00
Rhet Turnbull
9f8da5c623 Updated CHANGELOG.md [skip ci] 2021-07-24 21:30:03 -07:00
Rhet Turnbull
077d577c98 Fixed {album_seq} and {folder_album_seq} help text 2021-07-24 20:53:59 -07:00
Rhet Turnbull
12f39dbaf5 Added {album_seq} and {folder_album_seq}, #496 2021-07-24 20:41:31 -07:00
Rhet Turnbull
6e9f709279 Updated CHANGELOG.md [skip ci] 2021-07-23 06:14:35 -07:00
Rhet Turnbull
666b6cac33 Updated docs 2021-07-23 06:11:29 -07:00
Rhet Turnbull
e95c096784 Added {id} sequence number template, #154 2021-07-23 05:57:07 -07:00
Rhet Turnbull
745161fbd1 Updated CHANGELOG.md [skip ci] 2021-07-20 06:26:32 -07:00
Rhet Turnbull
8216c33b59 Updated example [skip ci] 2021-07-20 06:25:50 -07:00
Rhet Turnbull
a05e7be14e Updated test data 2021-07-20 06:09:40 -07:00
Rhet Turnbull
e27c40c772 Fixed album sort order for custom sort, #497 2021-07-20 05:41:13 -07:00
Rhet Turnbull
e752f3c7a7 Updated CHANGELOG.md [skip ci] 2021-07-18 21:09:39 -07:00
Rhet Turnbull
6f4cab6721 Updated example [skip ci] 2021-07-18 20:28:56 -07:00
Rhet Turnbull
2d899ef045 Pass dest_path to template function via RenderOptions, enable implementation of #496 2021-07-18 19:42:01 -07:00
Rhet Turnbull
4f17c8fb23 Updated CHANGELOG.md [skip ci] 2021-07-18 19:38:22 -07:00
Rhet Turnbull
173a0fce28 Added RenderOptions to {function} template, #496 2021-07-18 11:56:50 -07:00
Rhet Turnbull
b04ea8174d Added album_sort_order example 2021-07-18 09:07:16 -07:00
Rhet Turnbull
e40ecc45ad Update README.md 2021-07-18 07:57:34 -07:00
Rhet Turnbull
277b1614b9 Updated CHANGELOG.md [skip ci] 2021-07-16 20:09:48 -07:00
Rhet Turnbull
88099de688 Updated README.md [skip ci] 2021-07-16 20:07:50 -07:00
Rhet Turnbull
7d81b94c16 Upgraded osxmetadata to add new extended attributes 2021-07-16 19:45:19 -07:00
Rhet Turnbull
d627cfc4fa Update README.md 2021-07-15 20:25:28 -07:00
Rhet Turnbull
bf208bbe4b Updated tutorial with --regex example [skip ci] 2021-07-07 10:14:15 -07:00
Rhet Turnbull
79ba6f813f Updated CHANGELOG.md [skip ci] 2021-07-07 10:13:48 -07:00
Rhet Turnbull
141c0244e4 Added --selected, closes #489 2021-07-07 06:59:40 -07:00
Rhet Turnbull
7e0276beb7 Updated CHANGELOG.md [skip ci] 2021-07-06 22:03:48 -07:00
Rhet Turnbull
1bf11b0414 Fixed cleanup to delete empty folders, #491 2021-07-06 21:57:43 -07:00
allcontributors[bot]
c23f3fc5e4 docs: add mkirkland4874 as a contributor for example (#492)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-07-06 21:05:34 -07:00
Rhet Turnbull
016297d2ff Added example for {function} template 2021-07-06 21:00:16 -07:00
Rhet Turnbull
aa64283b55 Updated README.md [skip ci], closes #488 2021-07-06 12:17:02 -07:00
Rhet Turnbull
3973c27238 Updated CHANGELOG.md [skip ci] 2021-07-04 12:55:31 -07:00
Rhet Turnbull
2e32d62237 Added test for try/except block in cli export 2021-07-04 12:25:24 -07:00
Rhet Turnbull
d497b94ad5 Re-enabled try/except in cli export 2021-07-04 12:09:13 -07:00
Rhet Turnbull
8c09ae82a4 Updated CHANGELOG.md [skip ci] 2021-07-04 11:49:24 -07:00
Rhet Turnbull
632169f277 Added --preview-if-missing, #446 2021-07-04 10:03:36 -07:00
Rhet Turnbull
675371f0d7 Updated README.md [skip ci] 2021-07-04 08:41:58 -07:00
Rhet Turnbull
7e2d09bf12 Added --preview, #470 2021-07-04 08:39:06 -07:00
Rhet Turnbull
28c681aa96 Refactored export2, #485, #486 2021-07-03 22:50:03 -07:00
Rhet Turnbull
5d39aa92df Update README.md 2021-07-02 19:07:02 -07:00
Rhet Turnbull
b4dbad5e74 Fixed path_derivatives to always return jpeg if photo is a photo 2021-07-02 18:59:01 -07:00
Rhet Turnbull
b1b099257f Updated README.md [skip ci] 2021-07-02 13:27:22 -07:00
Rhet Turnbull
63e8410841 Updated CHANGELOG.md [skip ci] 2021-07-02 13:23:48 -07:00
Rhet Turnbull
2e1c91cd67 Added get_selected() to REPL 2021-07-02 13:19:15 -07:00
Rhet Turnbull
391b0a577b Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-07-02 13:01:28 -07:00
Rhet Turnbull
1d26ac9630 Removed _applescript, #461 2021-07-02 13:01:09 -07:00
Rhet Turnbull
03b4f59549 Removed _applescript, #461 2021-07-02 13:00:40 -07:00
Rhet Turnbull
9aa3ac3640 Updated CHANGELOG.md [skip ci] 2021-07-02 12:48:23 -07:00
Rhet Turnbull
6339e3c70e Updated README.md [skip ci] 2021-07-02 12:43:17 -07:00
Rhet Turnbull
4cc3220287 Fix for path_raw when file is reference, #480 2021-07-02 12:39:41 -07:00
Rhet Turnbull
f32c4f4acd Updated CHANGELOG.md [skip ci] 2021-06-30 22:54:34 -07:00
allcontributors[bot]
aba2ce0923 docs: add jcommisso07 as a contributor for data (#483)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-06-30 22:52:51 -07:00
allcontributors[bot]
c209ceae2e docs: add mkirkland4874 as a contributor for bug (#482)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-06-30 22:49:50 -07:00
Rhet Turnbull
94ac2bd04e Updated README.md [skip ci] 2021-06-30 22:43:56 -07:00
Rhet Turnbull
d1b1d20bcf Fixed --cleanup for empty export, #481 2021-06-30 22:41:03 -07:00
Rhet Turnbull
fb723fb8b7 Fixed raw+jpeg for Monterey 2021-06-29 18:22:48 -07:00
Rhet Turnbull
fc7c61b11b Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-06-29 17:47:36 -07:00
Rhet Turnbull
a73db3a1bb Updated photokit code to work with raw+jpeg, #478 2021-06-29 17:47:21 -07:00
Rhet Turnbull
d2dcbaaec4 Updated photokit code to work with raw+jpeg 2021-06-29 17:46:40 -07:00
Rhet Turnbull
08147e91d9 Alpha support for Monterey/macOS 12 2021-06-29 13:32:36 -07:00
Rhet Turnbull
d034605784 Refactored UTI utils to get ready for Monterey 2021-06-29 09:31:22 -07:00
Rhet Turnbull
64fd852535 Updated README.md [skip ci] 2021-06-23 22:43:36 -07:00
Rhet Turnbull
3fbfc55e84 Fixed deprecation warning 2021-06-23 22:40:23 -07:00
Rhet Turnbull
49317582c4 Bug fix for template functions #477 2021-06-23 22:36:58 -07:00
Rhet Turnbull
5ea01df69b Bug fix 2021-06-21 06:34:56 -07:00
Rhet Turnbull
4a9f8a9ef5 Updated CHANGELOG.md [skip ci] 2021-06-20 18:19:21 -07:00
Rhet Turnbull
49adff1f3b Updated example [skip ci] 2021-06-20 18:11:41 -07:00
Rhet Turnbull
377e165be4 Updated README.md [skip ci] 2021-06-20 17:56:48 -07:00
Rhet Turnbull
07da8031c6 Implemented --query-function, #430 2021-06-20 17:26:07 -07:00
Rhet Turnbull
be363b9727 Added query function [skip ci] 2021-06-20 16:38:51 -07:00
Rhet Turnbull
870a59a2fa Added --location, --no-location, #474 2021-06-20 15:33:03 -07:00
Rhet Turnbull
500cf71f7e Updated CHANGELOG.md [skip ci] 2021-06-20 15:31:44 -07:00
Rhet Turnbull
821e338b75 Fixed function names to work around Click.runner issue 2021-06-20 09:29:23 -07:00
Rhet Turnbull
987c91a9ff Implemented --post-function, #442 2021-06-20 08:52:45 -07:00
Rhet Turnbull
233942c9b6 Added post_function.py 2021-06-20 08:11:10 -07:00
Rhet Turnbull
a0ab64a841 Updated CHANGELOG.md [skip ci] 2021-06-19 21:56:01 -07:00
Rhet Turnbull
0cd8f32893 Bug fix for --download-missing, #456 2021-06-19 21:41:54 -07:00
Rhet Turnbull
904acbc576 Added isort cfg to match black 2021-06-19 18:03:05 -07:00
Rhet Turnbull
37dc023fcb Updated README.md [skip ci] 2021-06-19 18:02:32 -07:00
Rhet Turnbull
876ff17e3f Updated CHANGELOG.md [skip ci] 2021-06-19 17:49:47 -07:00
Rhet Turnbull
130df1a767 Updated README.md [skip ci] 2021-06-19 17:42:03 -07:00
Rhet Turnbull
5d7dea3fc3 Added repl command to CLI; closes #472 2021-06-19 17:31:02 -07:00
Rhet Turnbull
ca8397bc97 Updated CHANGELOG.md [skip ci] 2021-06-19 10:05:21 -07:00
Rhet Turnbull
91023ac8ec Added tutorial, closes #432 2021-06-19 09:59:43 -07:00
Rhet Turnbull
0ad59e9e29 Updated CHANGELOG.md [skip ci] 2021-06-18 22:14:14 -07:00
Rhet Turnbull
42c551de8a Updated help text, #469 2021-06-18 22:01:55 -07:00
Rhet Turnbull
62d49a7138 Updated README.md [skip ci] 2021-06-18 15:09:26 -07:00
Rhet Turnbull
bc5cd93e97 Added error handling for --add-to-album 2021-06-18 15:02:17 -07:00
Rhet Turnbull
7bd1ba8075 Updated CHANGELOG.md [skip ci] 2021-06-18 14:37:34 -07:00
Rhet Turnbull
64bb07a026 Added additional info to error message for --add-to-album 2021-06-18 14:03:59 -07:00
Rhet Turnbull
f1902b7fd4 Updated README.md [skip ci] 2021-06-18 13:10:06 -07:00
Rhet Turnbull
8e3f8fc7d0 Fix for #471 2021-06-18 13:05:37 -07:00
Rhet Turnbull
c588dcf0ba Updated CHANGELOG.md [skip ci] 2021-06-18 09:15:19 -07:00
Rhet Turnbull
fa29f51aeb Added --post-command, implements #443 2021-06-18 09:04:36 -07:00
Rhet Turnbull
ee0b369086 Added matrix for GitHub action OS 2021-06-18 08:49:39 -07:00
Rhet Turnbull
2fc45c2468 Added macos 10.15 and 11 2021-06-18 08:47:34 -07:00
Rhet Turnbull
15d2f45f0c Added macos 10.15 and 11 2021-06-18 08:46:21 -07:00
Rhet Turnbull
df7b73212f Added macos 10.15 and 11 2021-06-18 08:44:49 -07:00
Rhet Turnbull
5143b165b5 Updated CHANGELOG.md [skip ci] 2021-06-14 06:18:04 -07:00
Rhet Turnbull
10097323e5 Fixed missing more-itertools, #466 2021-06-14 05:16:56 -07:00
Rhet Turnbull
c0bd0ffc9f Added {filepath} template field in prep for --post-command and other goodies 2021-06-13 18:40:45 -07:00
Rhet Turnbull
2cdec3fc78 Refactored PhotoTemplate to support pathlib templates 2021-06-13 09:17:55 -07:00
Rhet Turnbull
1a46cdf63c Updated README.md [skip ci] 2021-06-12 22:08:34 -07:00
Rhet Turnbull
83892e096a Added --duplicate flag to find possible duplicates 2021-06-12 18:31:53 -07:00
Rhet Turnbull
6a0b8b4a3f version bump 2021-06-12 07:21:07 -07:00
Rhet Turnbull
5957fde809 Fixed cli status for --only-new and 0 photos to export 2021-06-12 07:20:39 -07:00
Rhet Turnbull
5711545b81 Fixed test for running in GitHub actions 2021-06-12 06:49:31 -07:00
Rhet Turnbull
0758f84dc4 Cleaned up tests, fixed bug in PhotosDB.query 2021-06-11 23:02:48 -07:00
Rhet Turnbull
4b6c35b5f9 Fix for --convert-to-jpeg with use_photos_export, #460 2021-06-09 04:00:05 -07:00
Rhet Turnbull
d7a9ad1d0a Refactored PhotoInfo.export2 2021-06-06 21:02:22 -07:00
Rhet Turnbull
bb96c35672 Updated test UUIDs 2021-06-06 13:58:44 -07:00
Rhet Turnbull
0880e5b9e8 added pyinstaller 2021-06-05 22:07:31 -07:00
Rhet Turnbull
87af23d98c Added python 3.9 to tests 2021-06-05 11:29:16 -07:00
Rhet Turnbull
61943d051b Updated dependencies to minimize pyobjc requirements 2021-06-05 11:25:41 -07:00
Rhet Turnbull
ef1daf5922 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-06-05 11:25:17 -07:00
Rhet Turnbull
bb98cff608 Test library update 2021-06-05 11:25:06 -07:00
Rhet Turnbull
620ba9ce03 Added dev_requirements.txt 2021-06-05 11:23:33 -07:00
Rhet Turnbull
86d94ad310 Added dev_requirements.txt 2021-06-05 11:22:37 -07:00
Rhet Turnbull
b8cf21ae82 Added venv [skip ci] 2021-06-04 07:01:33 -07:00
Rhet Turnbull
7accfdb066 Added PhotoInfo.duplicates 2021-06-01 17:32:43 -07:00
Rhet Turnbull
99f4394f8e Added CONTRIBUTING.md 2021-05-30 08:22:02 -07:00
Rhet Turnbull
748aed96cb Updated CHANGELOG.md [skip ci] 2021-05-29 09:08:46 -07:00
Rhet Turnbull
9161739ee6 Updated README.md [skip ci] 2021-05-29 09:05:05 -07:00
Rhet Turnbull
71cf8be94a Updated README.rst for PyPI 2021-05-29 09:03:35 -07:00
Rhet Turnbull
b48133cd83 Fix for #455 2021-05-29 08:51:58 -07:00
Rhet Turnbull
6b5a57fae9 Updated CHANGELOG.md [skip ci] 2021-05-28 09:09:26 -07:00
Rhet Turnbull
24ccf798c2 Updated README.md [skip ci] 2021-05-28 09:05:00 -07:00
Rhet Turnbull
a298772515 Updated tested versions to 11.3 2021-05-28 09:02:37 -07:00
Rhet Turnbull
2d68594b78 Fixes for #454 2021-05-28 08:48:21 -07:00
Rhet Turnbull
b026147c9a Updated README.md [skip ci] 2021-05-23 14:26:01 -07:00
Rhet Turnbull
186a5b77d0 Fixed bug in imageconverter exception handling, closes #440 2021-05-23 14:21:13 -07:00
Rhet Turnbull
518f855a9b PhotoInfo.exiftool now returns ExifToolCaching, closes #450 2021-05-23 14:14:22 -07:00
allcontributors[bot]
0d2067787c docs: add kaduskj as a contributor (#453)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-05-23 12:33:08 -07:00
Rhet Turnbull
0448a42329 Updated CHANGELOG.md [skip ci] 2021-05-23 12:30:10 -07:00
Rhet Turnbull
a724e15dd6 Updated README.md [skip ci] 2021-05-23 12:26:51 -07:00
Rhet Turnbull
be8fe9d059 Bug fix for #452 2021-05-23 12:01:36 -07:00
Rhet Turnbull
bd6656107b Updated CHANGELOG.md [skip ci] 2021-05-23 09:46:29 -07:00
Rhet Turnbull
a54e051d41 Updated README.md 2021-05-23 09:37:25 -07:00
Rhet Turnbull
7cde52bf9b Fixed #451, path_derivatives for Photos version <= 4 2021-05-23 09:34:40 -07:00
Rhet Turnbull
96037508c1 README.md update [skip ci] 2021-05-22 10:31:52 -07:00
Rhet Turnbull
9f2268fb2b Cleanup exiftool processes when exiting, #449 2021-05-19 06:16:52 -07:00
Rhet Turnbull
df167c00eb Added osxphotos related template fields, partial fix for #444 2021-05-15 14:46:35 -07:00
Rhet Turnbull
e8f9cda0c6 Update README.md 2021-05-09 18:21:38 -07:00
Rhet Turnbull
d4a951f547 Updated CHANGELOG.md, [skip ci] 2021-05-09 18:17:23 -07:00
Rhet Turnbull
f24e4a7e3c Updated path_derivatives to return results in sorted order (largest to smallest) 2021-05-09 17:52:24 -07:00
Rhet Turnbull
98b84c17f1 Updated CHANGELOG.md, [skip ci] 2021-05-08 23:18:04 -07:00
Rhet Turnbull
78c411a643 Updated docs 2021-05-08 23:10:47 -07:00
Rhet Turnbull
6bdf15b41e Added path_derivatives for Photos <= 4 2021-05-08 22:36:11 -07:00
Rhet Turnbull
a0fcec2a7a Fixed typo in README 2021-05-08 16:08:34 -07:00
Rhet Turnbull
63834ab8ab Added path_derivatives for Photos 5, issue #50 2021-05-08 15:11:59 -07:00
Rhet Turnbull
b23cfa32bb Updated docs 2021-05-07 23:00:17 -07:00
Rhet Turnbull
0e22ce54ab Added date_added for Photos 4, #439 2021-05-07 22:56:03 -07:00
Rhet Turnbull
0f41588701 Added date_added, #439 2021-05-05 06:50:41 -07:00
Rhet Turnbull
442b542794 Added --add-to-album example to README 2021-05-02 13:51:48 -07:00
Rhet Turnbull
88fae81b19 Updated CHANGELOG.md, [skip ci] 2021-05-02 13:51:26 -07:00
Rhet Turnbull
c4fec00f67 Updated docs [skip ci] 2021-05-02 09:15:13 -07:00
Rhet Turnbull
9a0cc3e8fa Added --add-to-album to query 2021-05-02 08:35:37 -07:00
Rhet Turnbull
3ed2362fe3 Updated CHANGELOG.md, [skip ci] 2021-05-01 21:26:49 -07:00
Rhet Turnbull
cd8dd552a4 Add --add-exported-to-album, # 428 2021-05-01 21:15:31 -07:00
Rhet Turnbull
64379f313e Update README.md 2021-04-29 00:53:04 -07:00
Rhet Turnbull
dc53480126 Updated known bugs section 2021-04-29 00:52:20 -07:00
Rhet Turnbull
3e06e0e344 Updated tutorial 2021-04-26 11:30:56 -07:00
Rhet Turnbull
aa9f6520d4 Updated tutorial 2021-04-26 11:30:31 -07:00
Rhet Turnbull
3a56e05c85 Updated tutorial 2021-04-26 11:27:12 -07:00
Rhet Turnbull
454a813908 Updated tutorial 2021-04-26 11:26:35 -07:00
Rhet Turnbull
31e162ba94 Add @pdewost as a contributor 2021-04-26 11:25:00 -07:00
Rhet Turnbull
5b8d51da38 Updated CHANGELOG.md, [skip ci] 2021-04-25 19:09:35 -07:00
Rhet Turnbull
846ea89012 Added PyCharm .idea/ 2021-04-25 18:53:31 -07:00
Rhet Turnbull
dc0bbd5fd6 Updated docs 2021-04-25 18:49:51 -07:00
Rhet Turnbull
91804d53ea Added read-only ExifToolCaching class, to implement #325 2021-04-25 18:44:48 -07:00
Rhet Turnbull
3d26206d91 Added normalized flag to ExifTool.asdict() 2021-04-25 08:31:08 -07:00
Rhet Turnbull
92d9dfaef2 Updated CHANGELOG.md, [skip ci] 2021-04-24 22:49:41 -07:00
Rhet Turnbull
51025e7f8b Added {edited_version} template field, closes #420 2021-04-24 22:20:37 -07:00
Rhet Turnbull
d52e5e9316 Updated CHANGELOG.md, [skip ci] 2021-04-24 20:01:40 -07:00
Rhet Turnbull
3711b3f7f1 Fixed handling of burst image selected/key/default, closes #401 (again) 2021-04-24 19:43:35 -07:00
Rhet Turnbull
48c229b52c Refactored export_photo to enable work on #420 2021-04-24 10:00:42 -07:00
Rhet Turnbull
aad435da36 Updated tutorial 2021-04-23 15:50:58 -07:00
Rhet Turnbull
9c60259089 Added instructions for using pre-built executable 2021-04-23 15:48:12 -07:00
Rhet Turnbull
131105d82c Fixed typo in tutorial 2021-04-23 15:36:56 -07:00
Rhet Turnbull
f54205ff49 Added tutorial to README 2021-04-23 15:29:49 -07:00
Rhet Turnbull
1d14fc8041 Refactored README.md to improve Template System section 2021-04-22 21:42:38 -07:00
Rhet Turnbull
4aec01ad1d Updated CHANGELOG.md, [skip ci] 2021-04-20 21:01:05 -07:00
dependabot[bot]
3630643a0e Bump py from 1.8.0 to 1.10.0 (#434)
Bumps [py](https://github.com/pytest-dev/py) from 1.8.0 to 1.10.0.
- [Release notes](https://github.com/pytest-dev/py/releases)
- [Changelog](https://github.com/pytest-dev/py/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/py/compare/1.8.0...1.10.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 20:34:25 -07:00
Rhet Turnbull
44966c6736 Added --regex query option, closes #433 2021-04-20 20:17:44 -07:00
Rhet Turnbull
7c4b28a35c Updated CHANGELOG.md, [skip ci] 2021-04-18 18:31:41 -07:00
Rhet Turnbull
1cdf4addad Fixed docs for function: filter 2021-04-18 18:05:26 -07:00
Rhet Turnbull
50fa851f23 Updated docs 2021-04-18 17:59:22 -07:00
Rhet Turnbull
a483b8a900 Version bump 2021-04-18 17:53:54 -07:00
Rhet Turnbull
dd6d519135 Added function filter to template system, closes #429 2021-04-18 17:52:14 -07:00
Rhet Turnbull
9371db094e Added template_filter.py to examples 2021-04-18 15:50:41 -07:00
Rhet Turnbull
d9a82f29c7 Updated README.md dependencies [skip ci] 2021-04-18 13:52:00 -07:00
Rhet Turnbull
3f57514fa3 Updated docs [skip ci] 2021-04-18 13:43:02 -07:00
Rhet Turnbull
a5a155bd05 Updated CHANGELOG.md, [skip ci] 2021-04-18 13:42:34 -07:00
Rhet Turnbull
c8ea0b0452 Added re to photosdb for use with query_eval 2021-04-18 13:24:02 -07:00
Rhet Turnbull
81fd51c793 Cleaned up queryoptions.py 2021-04-18 09:07:24 -07:00
Rhet Turnbull
648d399524 Updated CHANGELOG.md, [skip ci] 2021-04-18 09:05:28 -07:00
Rhet Turnbull
345c052353 Refactored _query to PhotosDB.query() 2021-04-18 08:32:13 -07:00
Rhet Turnbull
952f1a6c3c Fixed setup.py 2021-04-17 17:58:15 -07:00
Rhet Turnbull
7ae5b8aae7 Added --min-size, --max-size query options, #425 2021-04-17 17:56:48 -07:00
Rhet Turnbull
2e189d771e Updated docs, added build.sh 2021-04-17 10:00:34 -07:00
Rhet Turnbull
7fa7de1563 Added {newline}, #426 2021-04-17 09:29:11 -07:00
Rhet Turnbull
70d68a25ba Updated docs, closes #424 2021-04-17 03:03:23 -07:00
Rhet Turnbull
bfc4371d9e Updated CHANGELOG.md, [skip ci] 2021-04-17 02:57:55 -07:00
Rhet Turnbull
6a288676a1 Fixed bug for multi-field templates and --xattr-template, #422 2021-04-17 02:41:29 -07:00
Rhet Turnbull
874ad2fa34 Add @ubrandes as a contributor 2021-04-15 06:46:10 -07:00
Rhet Turnbull
a233167471 Updated CHANGELOG.md, [skip ci] 2021-04-14 22:17:00 -07:00
Rhet Turnbull
21dc0d388f Added {function} template, #419 2021-04-14 22:00:04 -07:00
Rhet Turnbull
eff8e7a63f Added template_function.py to examples 2021-04-14 20:20:46 -07:00
Rhet Turnbull
03f8b2bc6e Implements conditional expressions for template system, #417 2021-04-13 06:20:56 -07:00
Rhet Turnbull
e215c200c7 Updated CHANGELOG.md, [skip ci] 2021-04-11 23:54:40 -07:00
Rhet Turnbull
ae5b02f563 Added additional test for {photo} template 2021-04-11 23:49:54 -07:00
Rhet Turnbull
aa1a96d201 Added {photo} template, partial fix for issue #417 2021-04-11 23:36:17 -07:00
Rhet Turnbull
d9f24307ac Added {favorite} template, partial fix for #289 2021-04-11 19:45:50 -07:00
Rhet Turnbull
958f8c343a Doc updates 2021-04-09 07:36:02 -07:00
Rhet Turnbull
70cf4c9f92 Updated CHANGELOG.md, [skip ci] 2021-04-09 06:51:35 -07:00
Rhet Turnbull
2d3344ee34 Updated docs for --query-eval 2021-04-09 06:37:51 -07:00
Rhet Turnbull
b4bc906b6a Added --query-eval, implements #280 2021-04-08 22:15:58 -07:00
Rhet Turnbull
520a15fac6 Updated CHANGELOG.md, [skip ci] 2021-04-08 18:32:53 -07:00
Rhet Turnbull
032dff8967 Bug fix for #414, exiftool str replace 2021-04-05 13:11:17 -07:00
Rhet Turnbull
3c36b0fb33 Updated CHANGELOG.md, [skip ci] 2021-04-03 21:07:41 -07:00
Rhet Turnbull
d51d7a41e4 Added --name to search filename, closes #249, #412 2021-04-03 20:23:03 -07:00
Rhet Turnbull
60c926fea5 Updated CHANGELOG.md, [skip ci] 2021-04-03 08:03:10 -07:00
Rhet Turnbull
db27aac14b Added test for #409 2021-04-02 21:44:45 -07:00
Rhet Turnbull
d17454772c Update phototemplate.py
Fix for non-str values in exiftool template (#409)
2021-03-30 07:51:34 -06:00
dependabot[bot]
9c9e73ba96 Bump pygments from 2.6.1 to 2.7.4 (#408)
Bumps [pygments](https://github.com/pygments/pygments) from 2.6.1 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.6.1...2.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 07:37:28 -06:00
Rhet Turnbull
e21a78c2b3 Removed logging.debug code 2021-03-28 07:05:29 -07:00
Rhet Turnbull
de0fbf2bb9 Updated CHANGELOG.md, [skip ci] 2021-03-28 06:44:53 -07:00
Rhet Turnbull
b330e27fb8 Added --retry, issue #406 2021-03-27 22:40:56 -07:00
Rhet Turnbull
a941f66d62 Fixed albums for burst images, closes #401, #403, #404 2021-03-27 08:11:33 -07:00
dependabot[bot]
d77eba12b2 Bump pyyaml from 5.1.2 to 5.4 (#402)
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.1.2 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.1.2...5.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-25 21:06:24 -07:00
Rhet Turnbull
de94fd76de Updated CHANGELOG.md, [skip ci] 2021-03-21 23:18:41 -07:00
Rhet Turnbull
1026473684 Added --from-time, --to-time, closes #400 2021-03-21 22:57:18 -07:00
dependabot[bot]
3f9c9893c3 Bump pillow from 7.2.0 to 8.1.1 (#399)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.2.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.2.0...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-19 08:15:36 -07:00
Rhet Turnbull
574cdd65a3 Updated CHANGELOG.md, [skip ci] 2021-03-16 07:02:35 -07:00
Rhet Turnbull
5b9547669e Add --cleanup files to report, #395 2021-03-14 14:58:35 -07:00
allcontributors[bot]
35b5bbd13d docs: add AaronVanGeffen as a contributor (#398)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-03-14 12:19:26 -07:00
Aaron van Geffen
6870ad0d8e Use original filename to export photos by default (#396)
* Use original filename to export photos by default, #176

* Fixed edited filename to also use original_filename if none provided.

* Rolled back previous change due to test failures

* Use original filename for edited file exports as well

Co-authored-by: Rhet Turnbull <rturnbull@gmail.com>
2021-03-14 12:19:05 -07:00
Rhet Turnbull
17ac5949e1 Updated docs for --cleanup, #394 2021-03-14 12:16:26 -07:00
Rhet Turnbull
ffb9af1965 Fix for long descriptions with exiftool, #393 2021-03-14 08:38:07 -07:00
Rhet Turnbull
595307a003 Bug fix, convert PosixPath to str, #392 2021-03-04 23:46:25 -08:00
Rhet Turnbull
79a50b9e50 Updated CHANGELOG.md, [skip ci] 2021-02-21 23:13:56 -08:00
Rhet Turnbull
515df0a5dc Template refactor (#385)
* Initial implementation of new textx parser for template

* Implemented parser as singleton

* Moved grammar to .tx file

* Added filter templates

* Added filter templates

* Added tests for nested templates

* Added tests for filter+path_sep

* Added tests for filter+path_sep

* Added punctuation templates

* Added hook for --replace-keywords

* Updated docs for phototemplate

* Updated docs for phototemplate

* Updated tests data

* Updated tests data

* Updated docs for phototemplate

* Version bump

* Updated CLI help

* Fixed template processing for boolean, default
2021-02-21 20:19:51 -08:00
Rhet Turnbull
63bfa92563 Updated CHANGELOG.md, [skip ci] 2021-02-20 12:49:24 -08:00
Rhet Turnbull
44a1e3e7a7 Better exception handling for AdjustmentsInfo 2021-02-20 12:28:19 -08:00
Rhet Turnbull
6c84e476cc Updated CHANGELOG.md, [skip ci] 2021-02-20 11:35:15 -08:00
Rhet Turnbull
14fbe5e068 Merge pull request #383 from RhetTbull/all-contributors/add-neilpa
docs: add neilpa as a contributor
2021-02-20 11:05:19 -08:00
allcontributors[bot]
ebac9d0bfb docs: update .all-contributorsrc [skip ci] 2021-02-20 19:04:05 +00:00
allcontributors[bot]
29716c5272 docs: update README.md [skip ci] 2021-02-20 19:04:04 +00:00
Rhet Turnbull
fbe8229103 Version bump 2021-02-20 11:01:58 -08:00
Rhet Turnbull
5ee6affc05 Added AdjustmentsInfo, #150, #379 2021-02-20 11:01:08 -08:00
Rhet Turnbull
b3a7869bd3 Added depth_state to _info 2021-02-17 21:37:22 -08:00
Rhet Turnbull
e5f1c29974 Updated docs for --ignore-signature, #286 2021-02-14 11:37:32 -08:00
Rhet Turnbull
70848e1ff6 Removed orientation from XMP, #378 2021-02-13 18:58:33 -08:00
Rhet Turnbull
4b7a53faa8 Write description to ITPC:CaptionAbstract (#380) 2021-02-13 10:37:01 -08:00
Rhet Turnbull
a78dd80af4 Updated CHANGELOG.md, [skip ci] 2021-02-12 18:52:06 -08:00
Rhet Turnbull
1316866dc4 Added image orientation bug to Known Bugs 2021-02-12 08:24:24 -08:00
Rhet Turnbull
30273509d4 Fix for issue #366, --jpeg-ext, --convert-to-jpeg bug 2021-02-12 07:52:18 -08:00
Rhet Turnbull
15a3e69015 Updated CHANGELOG.md, [skip ci] 2021-02-10 06:50:56 -08:00
Rhet Turnbull
2691902d5c Added test for #374 2021-02-10 06:50:40 -08:00
Rhet Turnbull
da47821fae Bug fix for --jpeg-ext, #374 2021-02-09 22:17:20 -08:00
Rhet Turnbull
6f38e2da49 Updated CHANGELOG.md, [skip ci] 2021-02-08 22:07:17 -08:00
Rhet Turnbull
857e3db6cc Fixed --exiftool-option, #369, for real this time 2021-02-08 21:59:20 -08:00
Rhet Turnbull
7ed3115f36 Updated CHANGELOG.md, [skip ci] 2021-02-08 21:31:21 -08:00
Rhet Turnbull
198addaa07 Fixed --exiftool-option, #369 2021-02-08 21:21:30 -08:00
Rhet Turnbull
d91fc93737 Updated CHANGELOG.md, [skip ci] 2021-02-07 09:34:00 -08:00
Rhet Turnbull
5c3360f29d Fix for issue #366 2021-02-07 09:26:19 -08:00
Rhet Turnbull
d4513832a6 Updated CHANGELOG.md, [skip ci] 2021-02-07 09:05:59 -08:00
Rhet Turnbull
f8616acf16 Fixed unnecessary warning for long keywords, issue #365 2021-02-07 05:40:41 -08:00
Rhet Turnbull
addd952aa3 Implemented --in-album, --not-in-album, issue #364 2021-02-04 06:40:49 -08:00
Rhet Turnbull
773b619e24 Updated requirements.txt 2021-02-03 06:42:46 -08:00
Rhet Turnbull
683dfe7f3f Updated docs Makefile [skip ci] 2021-02-03 06:27:09 -08:00
Rhet Turnbull
7fa5fbaa5b Updated docs 2021-02-03 06:06:32 -08:00
Rhet Turnbull
e075868281 Updated CHANGELOG.md, [skip ci] 2021-02-02 22:14:41 -08:00
Rhet Turnbull
bf0589118b Version bump 2021-02-02 22:02:20 -08:00
Rhet Turnbull
adc4b05602 Added tests for --only-new, #358 2021-02-02 22:01:53 -08:00
Rhet Turnbull
a740d82e7f Updated XMP beta template 2021-02-02 21:18:00 -08:00
Rhet Turnbull
43af4d205a Fixed XMP template for issue #361 2021-02-02 21:16:12 -08:00
Rhet Turnbull
4d98fa9279 Merge pull request #362 from RhetTbull/dependabot/pip/bleach-3.3.0
Bump bleach from 3.1.4 to 3.3.0
2021-02-02 21:05:02 -08:00
dependabot[bot]
67e579be4c Bump bleach from 3.1.4 to 3.3.0
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.4 to 3.3.0.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.4...v3.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-02 22:57:26 +00:00
Rhet Turnbull
48d2223edd Updated tests for ExportDB, #358 2021-02-02 06:53:32 -08:00
Rhet Turnbull
591f9bcc62 Updated sidecar test data 2021-02-02 06:39:34 -08:00
Rhet Turnbull
2284598a24 Added 11.2 to tested versions, #360 2021-02-02 06:33:50 -08:00
Rhet Turnbull
77371b6e5d Fixed documentation, #359 2021-02-02 06:26:15 -08:00
Rhet Turnbull
8dbedef187 Add @davidjroos as a contributor 2021-02-02 06:23:19 -08:00
Rhet Turnbull
5c093c4352 Implemented --only-new, #358 2021-02-01 07:31:36 -08:00
Rhet Turnbull
e4fc3896f9 Fixed docs [skip ci] 2021-01-24 17:35:49 -08:00
Rhet Turnbull
2fb3fa3058 Fixed example for CLI restructure 2021-01-24 15:48:18 -08:00
Rhet Turnbull
3a4a8bdb0b Restructured docs 2021-01-24 08:27:23 -08:00
Rhet Turnbull
2fed1ebe5e Updated sphinx config 2021-01-23 22:36:35 -08:00
Rhet Turnbull
51f69585be Refactored __main__, added sphinx docs 2021-01-23 22:25:49 -08:00
Rhet Turnbull
8491830ee5 Updated CHANGELOG.md, [skip ci] 2021-01-23 09:24:48 -08:00
Rhet Turnbull
ebe2fc544d Fixed sidecar test data 2021-01-23 09:19:07 -08:00
Rhet Turnbull
4e47de7589 Version bump 2021-01-23 09:17:54 -08:00
Rhet Turnbull
5a696366fa Fix for issue #348 2021-01-23 09:17:13 -08:00
Rhet Turnbull
44153f0251 Updated CHANGELOG.md, [skip ci] 2021-01-22 21:03:30 -08:00
1001 changed files with 54937 additions and 16358 deletions

View File

@@ -166,6 +166,155 @@
"research",
"userTesting"
]
},
{
"login": "davidjroos",
"name": "davidjroos ",
"avatar_url": "https://avatars.githubusercontent.com/u/15630844?v=4",
"profile": "https://github.com/davidjroos",
"contributions": [
"doc"
]
},
{
"login": "neilpa",
"name": "Neil Pankey",
"avatar_url": "https://avatars.githubusercontent.com/u/42419?v=4",
"profile": "https://neilpa.me",
"contributions": [
"code"
]
},
{
"login": "AaronVanGeffen",
"name": "Aaron van Geffen",
"avatar_url": "https://avatars.githubusercontent.com/u/604665?v=4",
"profile": "https://aaronweb.net/",
"contributions": [
"code"
]
},
{
"login": "ubrandes",
"name": "ubrandes ",
"avatar_url": "https://avatars.githubusercontent.com/u/59647284?v=4",
"profile": "https://github.com/ubrandes",
"contributions": [
"ideas"
]
},
{
"login": "pdewost",
"name": "Philippe Dewost",
"avatar_url": "https://avatars.githubusercontent.com/u/17090228?v=4",
"profile": "http://blog.dewost.com/",
"contributions": [
"doc",
"example",
"ideas"
]
},
{
"login": "kaduskj",
"name": "kaduskj",
"avatar_url": "https://avatars.githubusercontent.com/u/983067?v=4",
"profile": "https://github.com/kaduskj",
"contributions": [
"bug"
]
},
{
"login": "mkirkland4874",
"name": "mkirkland4874",
"avatar_url": "https://avatars.githubusercontent.com/u/36466711?v=4",
"profile": "https://github.com/mkirkland4874",
"contributions": [
"bug",
"example"
]
},
{
"login": "jcommisso07",
"name": "Joseph Commisso",
"avatar_url": "https://avatars.githubusercontent.com/u/3111054?v=4",
"profile": "https://github.com/jcommisso07",
"contributions": [
"data"
]
},
{
"login": "dssinger",
"name": "David Singer",
"avatar_url": "https://avatars.githubusercontent.com/u/1817903?v=4",
"profile": "https://github.com/dssinger",
"contributions": [
"bug"
]
},
{
"login": "oPromessa",
"name": "oPromessa",
"avatar_url": "https://avatars.githubusercontent.com/u/21261491?v=4",
"profile": "https://github.com/oPromessa",
"contributions": [
"bug"
]
},
{
"login": "spencerc99",
"name": "Spencer Chang",
"avatar_url": "https://avatars.githubusercontent.com/u/14796580?v=4",
"profile": "http://spencerchang.me",
"contributions": [
"bug"
]
},
{
"login": "dgleich",
"name": "David Gleich",
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
"profile": "https://www.cs.purdue.edu/homes/dgleich",
"contributions": [
"code"
]
},
{
"login": "alandefreitas",
"name": "Alan de Freitas",
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
"profile": "https://alandefreitas.github.io/alandefreitas/",
"contributions": [
"bug"
]
},
{
"login": "hyfen",
"name": "Andrew Louis",
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
"profile": "https://hyfen.net",
"contributions": [
"doc",
"code"
]
},
{
"login": "neebah",
"name": "neebah",
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
"profile": "https://github.com/neebah",
"contributions": [
"bug"
]
},
{
"login": "ahti123",
"name": "Ahti Liin",
"avatar_url": "https://avatars.githubusercontent.com/u/22232632?v=4",
"profile": "https://github.com/ahti123",
"contributions": [
"code",
"bug"
]
}
],
"contributorsPerLine": 7,

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

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

View File

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

View File

@@ -4,13 +4,13 @@ on: [push, pull_request]
jobs:
build:
runs-on: macOS-latest
runs-on: ${{ matrix.os }}
if: "!contains(github.event.head_commit.message, '[skip ci]')"
strategy:
max-parallel: 4
matrix:
python-version: [3.7, 3.8]
os: [macos-10.15]
python-version: [3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v1
@@ -21,6 +21,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r dev_requirements.txt
pip install -r requirements.txt
# - name: Lint with flake8
# run: |
@@ -31,6 +32,4 @@ jobs:
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pip install pytest
pip install pytest-mock
python -m pytest tests/

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
.metrics
.DS_store
__pycache__
.coverage
.condaauto
t.out
.vscode/
.tox/
.idea/
dist/
build/
working/
osxphotos.egg-info/
.mypy_cache/
cli.spec
*.pyc
docsrc/_build/
venv/
.python-version

3
.isort.cfg Normal file
View File

@@ -0,0 +1,3 @@
[settings]
profile=black
multi_line_output=3

File diff suppressed because it is too large Load Diff

15
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributing
Contributions of all kinds are welcome! You don't need to know python to contribute to this project. For example, documentation updates are just as welcome as code!
Please explore open [issues](https://github.com/RhetTbull/osxphotos/issues), [discussions](https://github.com/RhetTbull/osxphotos/discussions), and the project [wiki](https://github.com/RhetTbull/osxphotos/wiki) to learn more about the project.
If you want to contribute source code, I recommend you explore the [wiki](https://github.com/RhetTbull/osxphotos/wiki/Structure-of-the-code) to learn about the source structure first.
See the [README.md](tests/README.md) in the tests directory before running any tests.
## Code of Conduct
Be nice to each other. Treat everyone with dignity and respect.
Abusive behavior of any kind will not be tolerated here.

View File

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

1869
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,9 @@ You can also easily export both the original and edited photos.
Supported operating systems
---------------------------
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
Beta support for macOS Big Sur (10.16.01/11.01).
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).
If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.
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.
@@ -109,6 +110,8 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
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.
To get help on a specific command, use ``osxphotos help <command_name>``
@@ -146,6 +149,11 @@ export default library using 'country name/year' as output directory (but use "N
``osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose``
find all videos larger than 200MB and add them to Photos album "Big Videos" creating the album if necessary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos query --only-movies --min-size 200MB --add-to-album "Big Videos"``
Example uses of the package
---------------------------

12
build.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script to help build osxphotos release
# this is unique to my own dev setup
# 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
python3 -m build
./make_cli_exe.sh

2
cli.py
View File

@@ -12,7 +12,7 @@
"""
from osxphotos.__main__ import cli
from osxphotos.cli import cli
if __name__ == "__main__":
cli()

10
dev_requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
build
m2r2
pyinstaller==4.4
pytest-mock
pytest==6.2.4
sphinx_click
sphinx_rtd_theme
twine
wheel
Sphinx

4
docs/.buildinfo Normal file
View File

@@ -0,0 +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: fff79f4920939baa44eddc90423972ec
tags: 645f666f9bcd5a90fca523b33c5a78b7

105
docs/_modules/index.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.43.9 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
<script src="../_static/jquery.js"></script>
<script src="../_static/underscore.js"></script>
<script src="../_static/doctools.js"></script>
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="stylesheet" href="../_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1>All modules for which code is available</h1>
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
</ul>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
<script src="../../../_static/jquery.js"></script>
<script src="../../../_static/underscore.js"></script>
<script src="../../../_static/doctools.js"></script>
<link rel="index" title="Index" href="../../../genindex.html" />
<link rel="search" title="Search" href="../../../search.html" />
<link rel="stylesheet" href="../../../_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1>Source code for osxphotos.photoinfo._photoinfo_exifinfo</h1><div class="highlight"><pre>
<span></span><span class="sd">&quot;&quot;&quot; PhotoInfo methods to expose EXIF info from the library &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
<span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ExifInfo</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot; EXIF info associated with a photo from the Photos library &quot;&quot;&quot;</span>
<span class="n">flash_fired</span><span class="p">:</span> <span class="nb">bool</span>
<span class="n">iso</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">metering_mode</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">sample_rate</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">track_format</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">white_balance</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">aperture</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">bit_rate</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">duration</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">exposure_bias</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">focal_length</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">fps</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">latitude</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">longitude</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">shutter_speed</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">camera_make</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">camera_model</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">codec</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">lens_model</span><span class="p">:</span> <span class="nb">str</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">exif_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns an ExifInfo object with the EXIF data for photo</span>
<span class="sd"> Note: the returned EXIF data is the data Photos stores in the database on import;</span>
<span class="sd"> ExifInfo does not provide access to the EXIF info in the actual image file</span>
<span class="sd"> Some or all of the fields may be None</span>
<span class="sd"> Only valid for Photos 5; on earlier database returns None</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;exif_info not implemented for this database version&quot;</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="n">exif</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">_db_exifinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">ExifInfo</span><span class="p">(</span>
<span class="n">iso</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZISO&quot;</span><span class="p">],</span>
<span class="n">flash_fired</span><span class="o">=</span><span class="kc">True</span> <span class="k">if</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZFLASHFIRED&quot;</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="p">,</span>
<span class="n">metering_mode</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZMETERINGMODE&quot;</span><span class="p">],</span>
<span class="n">sample_rate</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZSAMPLERATE&quot;</span><span class="p">],</span>
<span class="n">track_format</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZTRACKFORMAT&quot;</span><span class="p">],</span>
<span class="n">white_balance</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZWHITEBALANCE&quot;</span><span class="p">],</span>
<span class="n">aperture</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZAPERTURE&quot;</span><span class="p">],</span>
<span class="n">bit_rate</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZBITRATE&quot;</span><span class="p">],</span>
<span class="n">duration</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZDURATION&quot;</span><span class="p">],</span>
<span class="n">exposure_bias</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZEXPOSUREBIAS&quot;</span><span class="p">],</span>
<span class="n">focal_length</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZFOCALLENGTH&quot;</span><span class="p">],</span>
<span class="n">fps</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZFPS&quot;</span><span class="p">],</span>
<span class="n">latitude</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZLATITUDE&quot;</span><span class="p">],</span>
<span class="n">longitude</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZLONGITUDE&quot;</span><span class="p">],</span>
<span class="n">shutter_speed</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZSHUTTERSPEED&quot;</span><span class="p">],</span>
<span class="n">camera_make</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZCAMERAMAKE&quot;</span><span class="p">],</span>
<span class="n">camera_model</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZCAMERAMODEL&quot;</span><span class="p">],</span>
<span class="n">codec</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZCODEC&quot;</span><span class="p">],</span>
<span class="n">lens_model</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">&quot;ZLENSMODEL&quot;</span><span class="p">],</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Could not find exif record 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">&quot;</span><span class="p">)</span>
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">ExifInfo</span><span class="p">(</span>
<span class="n">iso</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">flash_fired</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">metering_mode</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">sample_rate</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">track_format</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">white_balance</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">aperture</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">bit_rate</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">duration</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">exposure_bias</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">focal_length</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">fps</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">latitude</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">longitude</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">shutter_speed</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">camera_make</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">camera_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">codec</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">lens_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">exif_info</span>
</pre></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../../../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../../../index.html">Documentation overview</a><ul>
<li><a href="../../index.html">Module code</a><ul>
</ul></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,220 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_scoreinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
<script src="../../../_static/jquery.js"></script>
<script src="../../../_static/underscore.js"></script>
<script src="../../../_static/doctools.js"></script>
<link rel="index" title="Index" href="../../../genindex.html" />
<link rel="search" title="Search" href="../../../search.html" />
<link rel="stylesheet" href="../../../_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1>Source code for osxphotos.photoinfo._photoinfo_scoreinfo</h1><div class="highlight"><pre>
<span></span><span class="sd">&quot;&quot;&quot; PhotoInfo methods to expose computed score info from the library &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
<span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ScoreInfo</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot; Computed photo score info associated with a photo from the Photos library &quot;&quot;&quot;</span>
<span class="n">overall</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">curation</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">promotion</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">highlight_visibility</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">behavioral</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">failure</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">harmonious_color</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">immersiveness</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">interaction</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">interesting_subject</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">intrusive_object_presence</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">lively_color</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">low_light</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">noise</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_camera_tilt</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_composition</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_lighting</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_pattern</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_perspective</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_post_processing</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_reflection</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">pleasant_symmetry</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">sharply_focused_subject</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">tastefully_blurred</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">well_chosen_subject</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">well_framed_subject</span><span class="p">:</span> <span class="nb">float</span>
<span class="n">well_timed_shot</span><span class="p">:</span> <span class="nb">float</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">score</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Computed score information for a photo</span>
<span class="sd"> Returns:</span>
<span class="sd"> ScoreInfo instance</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;score not implemented for this database version&quot;</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">_scoreinfo</span> <span class="c1"># pylint: disable=access-member-before-definition</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">scores</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">_db_scoreinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span> <span class="o">=</span> <span class="n">ScoreInfo</span><span class="p">(</span>
<span class="n">overall</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;overall_aesthetic&quot;</span><span class="p">],</span>
<span class="n">curation</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;curation&quot;</span><span class="p">],</span>
<span class="n">promotion</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;promotion&quot;</span><span class="p">],</span>
<span class="n">highlight_visibility</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;highlight_visibility&quot;</span><span class="p">],</span>
<span class="n">behavioral</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;behavioral&quot;</span><span class="p">],</span>
<span class="n">failure</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;failure&quot;</span><span class="p">],</span>
<span class="n">harmonious_color</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;harmonious_color&quot;</span><span class="p">],</span>
<span class="n">immersiveness</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;immersiveness&quot;</span><span class="p">],</span>
<span class="n">interaction</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;interaction&quot;</span><span class="p">],</span>
<span class="n">interesting_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;interesting_subject&quot;</span><span class="p">],</span>
<span class="n">intrusive_object_presence</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;intrusive_object_presence&quot;</span><span class="p">],</span>
<span class="n">lively_color</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;lively_color&quot;</span><span class="p">],</span>
<span class="n">low_light</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;low_light&quot;</span><span class="p">],</span>
<span class="n">noise</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;noise&quot;</span><span class="p">],</span>
<span class="n">pleasant_camera_tilt</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_camera_tilt&quot;</span><span class="p">],</span>
<span class="n">pleasant_composition</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_composition&quot;</span><span class="p">],</span>
<span class="n">pleasant_lighting</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_lighting&quot;</span><span class="p">],</span>
<span class="n">pleasant_pattern</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_pattern&quot;</span><span class="p">],</span>
<span class="n">pleasant_perspective</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_perspective&quot;</span><span class="p">],</span>
<span class="n">pleasant_post_processing</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_post_processing&quot;</span><span class="p">],</span>
<span class="n">pleasant_reflection</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_reflection&quot;</span><span class="p">],</span>
<span class="n">pleasant_symmetry</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;pleasant_symmetry&quot;</span><span class="p">],</span>
<span class="n">sharply_focused_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;sharply_focused_subject&quot;</span><span class="p">],</span>
<span class="n">tastefully_blurred</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;tastefully_blurred&quot;</span><span class="p">],</span>
<span class="n">well_chosen_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;well_chosen_subject&quot;</span><span class="p">],</span>
<span class="n">well_framed_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;well_framed_subject&quot;</span><span class="p">],</span>
<span class="n">well_timed_shot</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">&quot;well_timed_shot&quot;</span><span class="p">],</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</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">_scoreinfo</span> <span class="o">=</span> <span class="n">ScoreInfo</span><span class="p">(</span>
<span class="n">overall</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">curation</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">promotion</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">highlight_visibility</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">behavioral</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">failure</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">harmonious_color</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">immersiveness</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">interaction</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">interesting_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">intrusive_object_presence</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">lively_color</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">low_light</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">noise</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_camera_tilt</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_composition</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_lighting</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_pattern</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_perspective</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_post_processing</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_reflection</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">pleasant_symmetry</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">sharply_focused_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">tastefully_blurred</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">well_chosen_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">well_framed_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="n">well_timed_shot</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span>
</pre></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../../../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../../../index.html">Documentation overview</a><ul>
<li><a href="../../index.html">Module code</a><ul>
</ul></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_searchinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
<script src="../../../_static/jquery.js"></script>
<script src="../../../_static/underscore.js"></script>
<script src="../../../_static/doctools.js"></script>
<link rel="index" title="Index" href="../../../genindex.html" />
<link rel="search" title="Search" href="../../../search.html" />
<link rel="stylesheet" href="../../../_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1>Source code for osxphotos.photoinfo._photoinfo_searchinfo</h1><div class="highlight"><pre>
<span></span><span class="sd">&quot;&quot;&quot; Methods and class for PhotoInfo exposing SearchInfo data such as labels </span>
<span class="sd"> Adds the following properties to PhotoInfo (valid only for Photos 5):</span>
<span class="sd"> search_info: returns a SearchInfo object</span>
<span class="sd"> search_info_normalized: returns a SearchInfo object with properties that produce normalized results</span>
<span class="sd"> labels: returns list of labels</span>
<span class="sd"> labels_normalized: returns list of normalized labels</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_PHOTOS_4_VERSION</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_CITY</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_LABEL</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_NEIGHBORHOOD</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_PLACE_NAME</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_STREET</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_ALL_LOCALITY</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_COUNTRY</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_STATE</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_STATE_ABBREVIATION</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_BODY_OF_WATER</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_MONTH</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_YEAR</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_HOLIDAY</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_ACTIVITY</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_SEASON</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_VENUE</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_VENUE_TYPE</span><span class="p">,</span>
<span class="n">SEARCH_CATEGORY_MEDIA_TYPES</span><span class="p">,</span>
<span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">search_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns SearchInfo object for photo </span>
<span class="sd"> only valid on Photos 5, on older libraries, returns None</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="c1"># memoize SearchInfo object</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">_search_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_search_info</span> <span class="o">=</span> <span class="n">SearchInfo</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_search_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">search_info_normalized</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns SearchInfo object for photo that produces normalized results</span>
<span class="sd"> only valid on Photos 5, on older libraries, returns None</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="c1"># memoize SearchInfo object</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">_search_info_normalized</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_search_info_normalized</span> <span class="o">=</span> <span class="n">SearchInfo</span><span class="p">(</span><span class="bp">self</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">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_search_info_normalized</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">labels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of labels applied to photo by Photos image categorization</span>
<span class="sd"> only valid on Photos 5, on older libraries returns empty list </span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info</span><span class="o">.</span><span class="n">labels</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">labels_normalized</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns normalized list of labels applied to photo by Photos image categorization</span>
<span class="sd"> only valid on Photos 5, on older libraries returns empty list </span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info_normalized</span><span class="o">.</span><span class="n">labels</span>
<span class="k">class</span> <span class="nc">SearchInfo</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot; Info about search terms such as machine learning labels that Photos knows about a photo &quot;&quot;&quot;</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">photo</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; photo: PhotoInfo object</span>
<span class="sd"> normalized: if True, all properties return normalized (lower case) results &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;search info not implemented for this database version&quot;</span>
<span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="o">=</span> <span class="n">photo</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_normalized</span> <span class="o">=</span> <span class="n">normalized</span>
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">uuid</span>
<span class="k">try</span><span class="p">:</span>
<span class="c1"># get search info for this UUID</span>
<span class="c1"># there might not be any search info data (e.g. if Photo was missing or photoanalysisd not run yet)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_searchinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span> <span class="o">=</span> <span class="kc">None</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">labels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of labels associated with Photo &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_LABEL</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">place_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of place names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_PLACE_NAME</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">streets</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of street names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STREET</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">neighborhoods</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of neighborhoods &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_NEIGHBORHOOD</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">locality_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of other locality names &quot;&quot;&quot;</span>
<span class="n">locality</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">category</span> <span class="ow">in</span> <span class="n">SEARCH_CATEGORY_ALL_LOCALITY</span><span class="p">:</span>
<span class="n">locality</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">category</span><span class="p">)</span>
<span class="k">return</span> <span class="n">locality</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">city</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns city/town &quot;&quot;&quot;</span>
<span class="n">city</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_CITY</span><span class="p">)</span>
<span class="k">return</span> <span class="n">city</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">city</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">state</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns state name &quot;&quot;&quot;</span>
<span class="n">state</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STATE</span><span class="p">)</span>
<span class="k">return</span> <span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">state</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">state_abbreviation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns state abbreviation &quot;&quot;&quot;</span>
<span class="n">abbrev</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STATE_ABBREVIATION</span><span class="p">)</span>
<span class="k">return</span> <span class="n">abbrev</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">abbrev</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">country</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns country name &quot;&quot;&quot;</span>
<span class="n">country</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_COUNTRY</span><span class="p">)</span>
<span class="k">return</span> <span class="n">country</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">country</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">month</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns month name &quot;&quot;&quot;</span>
<span class="n">month</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_MONTH</span><span class="p">)</span>
<span class="k">return</span> <span class="n">month</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">month</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">year</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns year &quot;&quot;&quot;</span>
<span class="n">year</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_YEAR</span><span class="p">)</span>
<span class="k">return</span> <span class="n">year</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">year</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">bodies_of_water</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of body of water names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_BODY_OF_WATER</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">holidays</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of holiday names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_HOLIDAY</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">activities</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of activity names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_ACTIVITY</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">season</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns season name &quot;&quot;&quot;</span>
<span class="n">season</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_SEASON</span><span class="p">)</span>
<span class="k">return</span> <span class="n">season</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">season</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">venues</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of venue names &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_VENUE</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">venue_types</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of venue types &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_VENUE_TYPE</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">media_types</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns list of media types (photo, video, panorama, etc) &quot;&quot;&quot;</span>
<span class="n">types</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">category</span> <span class="ow">in</span> <span class="n">SEARCH_CATEGORY_MEDIA_TYPES</span><span class="p">:</span>
<span class="n">types</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">category</span><span class="p">)</span>
<span class="k">return</span> <span class="n">types</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">all</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return all search info properties in a single list &quot;&quot;&quot;</span>
<span class="nb">all</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">labels</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">place_names</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">streets</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">neighborhoods</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">locality_names</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">bodies_of_water</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">holidays</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">activities</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">venues</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">venue_types</span>
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">media_types</span>
<span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">:</span>
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">]</span>
<span class="k">return</span> <span class="nb">all</span>
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return dict of search info &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;labels&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">labels</span><span class="p">,</span>
<span class="s2">&quot;place_names&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">place_names</span><span class="p">,</span>
<span class="s2">&quot;streets&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">streets</span><span class="p">,</span>
<span class="s2">&quot;neighborhoods&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">neighborhoods</span><span class="p">,</span>
<span class="s2">&quot;city&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">,</span>
<span class="s2">&quot;locality_names&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">locality_names</span><span class="p">,</span>
<span class="s2">&quot;state&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">,</span>
<span class="s2">&quot;state_abbreviation&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">,</span>
<span class="s2">&quot;country&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">,</span>
<span class="s2">&quot;bodies_of_water&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">bodies_of_water</span><span class="p">,</span>
<span class="s2">&quot;month&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">,</span>
<span class="s2">&quot;year&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">,</span>
<span class="s2">&quot;holidays&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">holidays</span><span class="p">,</span>
<span class="s2">&quot;activities&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">activities</span><span class="p">,</span>
<span class="s2">&quot;season&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">,</span>
<span class="s2">&quot;venues&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">venues</span><span class="p">,</span>
<span class="s2">&quot;venue_types&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">venue_types</span><span class="p">,</span>
<span class="s2">&quot;media_types&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">media_types</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">category</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of text for a specified category ID &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span><span class="p">:</span>
<span class="n">content</span> <span class="o">=</span> <span class="s2">&quot;normalized_string&quot;</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_normalized</span> <span class="k">else</span> <span class="s2">&quot;content_string&quot;</span>
<span class="k">return</span> <span class="p">[</span>
<span class="n">rec</span><span class="p">[</span><span class="n">content</span><span class="p">]</span>
<span class="k">for</span> <span class="n">rec</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span>
<span class="k">if</span> <span class="n">rec</span><span class="p">[</span><span class="s2">&quot;category&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">category</span>
<span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
</pre></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../../../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../../../index.html">Documentation overview</a><ul>
<li><a href="../../index.html">Module code</a><ul>
</ul></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
osxphotos command line interface (CLI)
======================================
.. click:: osxphotos.cli:cli
:prog: osxphotos
:nested: full

View File

@@ -0,0 +1,23 @@
.. osxphotos documentation master file, created by
sphinx-quickstart on Sat Jan 23 13:27:27 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to osxphotos's documentation!
=====================================
.. include:: ../../README.rst
.. toctree::
:maxdepth: 4
cli
reference
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -0,0 +1,5 @@
osxphotos
===========
.. toctree::
:maxdepth: 4

View File

@@ -0,0 +1,13 @@
osxphotos package
===================
osxphotos module
------------------------------
.. autoclass:: osxphotos.PhotosDB
:members:
:undoc-members:
.. autoclass:: osxphotos.PhotoInfo
:members:
:undoc-members:

View File

@@ -0,0 +1,354 @@
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
# OSXPhotos Tutorial
## Tutorial
<!-- OSXPHOTOS-TUTORIAL-HEADER:END -->
The design philosophy for osxphotos is "make the easy things easy and make the hard things possible". To "make the hard things possible", osxphotos is very flexible and has many, many configuration options -- the `export` command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the `export` command which exports photos from the library so that's the focus of this tutorial.
### Export your photos
`osxphotos export /path/to/export`
This command exports all your photos to the `/path/to/export` directory.
**Note**: osxphotos uses the term 'photo' to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple "Live Photo" which is an image and an associated "live preview" video file), a JPEG image with an associated RAW image, etc.
### Export by date
While the previous command will export all your photos (and videos--see note above), it probably doesn't do exactly what you want. In the previous example, all the photos will be exported to a single folder: `/path/to/export`. If you have a large library with thousands of images and videos, this likely isn't very useful. You can use the `--export-by-date` option to export photos to a folder structure organized by year, month, day, e.g. `2021/04/21`:
`osxphotos export /path/to/export --export-by-date`
With this command, a photo that was created on 31 May 2015 would be exported to: `/path/to/export/2015/05/31`
### Specify directory structure
If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START -->template system<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --> that allows you to specify the directory structure using the `--directory` option. For example, this command exported to a directory structure that looks like: `2015/May` (4-digit year / month name):
`osxphotos export /path/to/export --directory "{created.year}/{created.month}"`
The string following `--directory` is an `osxphotos template string`. Template strings are widely used throughout osxphotos and it's worth your time to learn more about them. In a template string, the values between the curly braces, e.g. `{created.year}` are replaced with metadata from the photo being exported. In this case, `{created.year}` is the 4-digit year of the photo's creation date and `{created.month}` is the full month name in the user's locale (e.g. `May`, `mai`, etc.). In the osxphotos template system these are referred to as template fields. The text not included between `{}` pairs is interpreted literally, in this case `/`, is a directory separator.
osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the `--directory` template, you could thus export photos organized by country name:
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country}"`
Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country,No-Country}"`
The value after the ',' in the template string is the default value, in this case 'No-Country'. **Note**: If you don't specify a default value and a template field is null, osxphotos will use "_" (underscore character) as the default.
Some template fields, such as `{keyword}`, may expand to more than one value. For example, if a photo has keywords of "Travel" and "Vacation", `{keyword}` would expand to "Travel", "Vacation". When used with `--directory`, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
`osxphotos export /path/to/export --directory "{keyword}"`
the exported files would be:
/path/to/export/Travel/IMG_1234.JPG
/path/to/export/Vacation/IMG_1234.JPG
### Specify exported filename
By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like `IMG_1234.JPG` or `DSC05678.dng`. osxphotos allows you to specify a custom filename template using the `--filename` option in the same way as `--directory` allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:
`osxphotos export /path/to/export --filename "{title}"`
The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
```txt
osxphotos export /path/to/export --filename "{title,{original_name}}"
│ ││ │
│ ││ │
Use photo's title as the filename <──────┘ ││ │
││ │
Value after comma will be used <───────┘│ │
if title is blank │ │
│ │
The default value can be <────┘ │
another template field │
Use photo's original name if no title <──────┘
```
The osxphotos template system also allows for limited conditional logic of the type "If a condition is true then do one thing, otherwise, do a different thing". For example, you can use the `--filename` option to name files that are marked as "Favorites" in Photos differently than other files. For example, to add a "#" to the name of every photo that's a favorite:
```txt
osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
│ │ │││
│ │ │││
Use photo's original name as filename <──┘ │ │││
│ │││
'favorite' is True if photo is a Favorite, <───────┘ │││
otherwise, False │││
│││
'?' specifies a conditional <─────────────┘││
││
Value immediately following ? will be used if <──────┘│
preceding template field is True or non-blank │
Value immediately following comma will be used if <──────┘
template field is False or blank (null); in this case
no value is specified so a blank string "" will be used
```
Like with `--directory`, using a multi-valued template field such as `{keyword}` may result in more than one copy of a photo being exported. For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
`osxphotos export /path/to/export --filename "{keyword}-{original_name}"`
the exported files would be:
/path/to/export/Travel-IMG_1234.JPG
/path/to/export/Vacation-IMG_1234.JPG
### Edited photos
If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append "_edited" to the edited image. For example, if the original image was named `IMG_1234.JPG`, osxphotos will export the original as `IMG_1234.JPG` and the edited version as `IMG_1234_edited.jpeg`. **Note:** Photos changes the extension of edited images to ".jpeg" even if the original was named ".JPG". You can change the suffix appended to edited images using the `--edited-suffix` option:
`osxphotos export /path/to/export --edited-suffix "_EDIT"`
In this example, the edited image would be named `IMG_1234_EDIT.jpeg`. Like many options in osxphotos, the `--edited-suffix` option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:
`osxphotos export /path/to/export --edited-suffix "_{modified.year}-{modified.mm}-{modified.dd}"`
In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: `IMG_1234_2021-04-21.jpeg`.
You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using `--skip-edited`:
`osxphotos export /path/to/export --skip-edited`
You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the `--skip-original-if-edited` option:
`osxphotos export /path/to/export --skip-original-if-edited`
As mentioned above, Photos renames JPEG images that have been edited with the ".jpeg" extension. Some applications use ".JPG" and others use ".jpg" or ".JPEG". You can use the `--jpeg-ext` option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. `--jpeg-ext jpg` to use '.jpg' for all JPEGs.
`osxphotos export /path/to/export --jpeg-ext jpg`
### Specifying the Photos library
All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the "Option" key while opening Photos, you can select an alternate Photos library. If you don't specify which library to use, osxphotos will try find the last opened library. Occasionally it can't determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the `--db` option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).
`osxphotos export /path/to/export --db ~/Pictures/MyAlternateLibrary.photoslibrary`
### Missing photos
osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.
If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the `--download-missing` option. `--download-missing` uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos' AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There's also an experimental `--use-photokit` option that will communicate with Photos using a different "PhotoKit" interface. This option must be used together with `--download-missing`:
`osxphotos export /path/to/export --download-missing`
`osxphotos export /path/to/export --download-missing --use-photokit`
### Exporting to external disks
If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the `--retry` option so that osxphotos will automatically retry the export. Use `--retry` with a number that specifies the number of times to retry the export:
`osxphotos export /path/to/export --retry 3`
In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.
### Exporting metadata with exported photos
Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos' native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free [`exiftool`](https://exiftool.org/) app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the `--exiftool` option to write metadata to exported photos:
`osxphotos export /path/to/export --exiftool`
This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with `--exiftool` to modify the metadata that is written by `exiftool`. For example, you can use the `--keyword-template` option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:
```txt
osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
│ │
│ │
folder_album results in the folder(s) <──┘ │
and album a photo is contained in │
The value in () is used as the path separator <───────┘
for joining the folders and albums. For example,
if photo is in Folder1/Folder2/Album, (>) produces
"Folder1>Folder2>Album" which some programs, such as
Lightroom Classic, treat as hierarchal keywords
```
The above command will write all the regular metadata that `--exiftool` normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form "Folder1>Folder2>Album". If you did not include the `(>)` in the template string (e.g. `{folder_album}`), folder_album would render in form "Folder1/Folder2/Album".
A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the `{label}`. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image's metadata:
`osxphotos export /path/to/export --exiftool --keyword-template "{label}"`
**Note**: When evaluating templates for `--directory` and `--filename`, osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as `--keyword-template`, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
`osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"`
### Sidecar files
Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the `--sidecar` option. For example, to output metadata to XMP sidecars:
`osxphotos export /path/to/export --sidecar XMP`
Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`.
Sidecar files are named "photoname.ext.sidecar_ext". For example, if the photo is named `IMG_1234.JPG` and the sidecar format is XMP, the sidecar would be named `IMG_1234.JPG.XMP`. Some applications expect the sidecar in this case to be named `IMG_1234.XMP`. You can use the `-sidecar-drop-ext` option to force osxphotos to name the sidecar files in this manner:
`osxphotos export /path/to/export --sidecar XMP -sidecar-drop-ext`
### Updating a previous export
If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the `--update` option. When `osxphotos export` is run, it creates a database file named `.osxphotos_export.db` in the export folder. (**Note** Because the filename starts with a ".", you won't see it in Finder which treats "dot-files" like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the `--update` option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:
`osxphotos export /path/to/export --update`
will read the export database located in `/path/to/export/.osxphotos_export.db` and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the `--update` option even if it's never been run before. If the database isn't found, osxphotos will create it. If you run `osxphotos export` without `--update` in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use `--update`.
If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of `--update`, you can use the `--only-new` with `--update` to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without `--only-new`, osxphotos would see that previously exported files are missing and re-export them.
`osxphotos export /path/to/export --update --only-new`
If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with `--update`, you should use the `--ignore-signature` option. `--ignore-signature` instructs osxphotos to ignore the file's signature (for example, size and date modified) when deciding which files should be updated with `--update`. If you edit a file in the export directory and then run `--update` without `--ignore-signature`, osxphotos will see that the file is different than the one in the Photos library and re-export it.
`osxphotos export /path/to/export --update --ignore-signature`
### Dry Run
You can use the `--dry-run` option to have osxphotos "dry run" or test an export without actually exporting any files. When combined with the `--verbose` option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your `--directory` and `--filename` templates are correct, `--dry-run --verbose` will print out the name of each file being exported.
`osxphotos export /path/to/export --dry-run --verbose`
### Creating a report of all exported files
You can use the `--report` option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the `--report` option:
`osxphotos export /path/to/export --report export.csv`
### Exporting only certain photos
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
For example, to export only photos with keyword `Travel`:
`osxphotos export /path/to/export --keyword "Travel"`
Like many options in osxphotos, `--keyword` (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword `Travel` *or* keyword `Vacation`:
`osxphotos export /path/to/export --keyword "Travel" --keyword "Vacation"`
To export only photos contained in the album "Summer Vacation":
`osxphotos export /path/to/export --album "Summer Vacation"`
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
`osxphotos export /path/to/export --portrait`
You can also export photos in a certain date range:
`osxphotos export /path/to/export --from-date "2020-01-01" --to-date "2020-02-28"`
### Converting images to JPEG on export
Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the `--convert-to-jpeg` option. You can specify the JPEG quality (0: worst, 1.0: best) using `--jpeg-quality`. For example:
`osxphotos export /path/to/export --convert-to-jpeg --jpeg-quality 0.9`
### Finder attributes
In addition to using `exiftool` to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.
osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the `tag:tagname` syntax:
`osxphotos export /path/to/export --finder-tag-keywords`
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
`osxphotos export /path/to/export --finder-tag-keywords --keyword-template "{label}"`
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
For example, to set Finder comment to the photo's title and description:
`osxphotos export /path/to/export --xattr-template findercomment "{title}{newline}{descr}"`
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
Explanation of the template string:
```txt
{title}{title?{descr?{newline},},}{descr}
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──> insert title │ │ │ │ │
│ │ │ │ │ │
└───> is there a title?
│ │ │ │ │
└───> if so, is there a description?
│ │ │ │
└───> if so, insert new line
│ │ │
└───> if descr is blank, insert nothing
│ │
└───> if title is blank, insert nothing
└───> finally, insert description
```
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.
See Extended Attributes section in the help for `osxphotos export` for additional information about this feature.
### Saving and loading options
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
To save the configuration:
`osxphotos export /path/to/export <all your options here> --update --save-config osxphotos.toml`
Then the next to you run osxphotos, you can simply do this:
`osxphotos export /path/to/export --load-config osxphotos.toml`
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
### An example from an actual osxphotos user
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
I usually import my iPhones photo roll on a more or less regular basis, and it
includes photos and videos. As a result, the size ot my Photos library may rise
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
quite good keyword management system.
After a while, I want to take most of the videos out of the library and move them
to a separate "videos" folder on a different folder / volume. As I might want to
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
its internal library tagging system, I will use osxphotos to do just this.
Picking the videos can be left to Photos, using a smart folder for instance. Then
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
to spot all videos created on my iPhone using the Quik application (now part of
GoPro).
I want to retrieve my keywords only and make sure they populate the Finder tags, as
well as export all the persons identified in the videos by Photos. I also want to
merge any keywords or persons already in the video metadata with the exported
metadata.
Keeping Photos edited titles and descriptions and putting both in the Finder
comments field in a readable manner is also enabled.
And I want to keep the files creation date (using `--touch-file`).
Finally, use `--strip` to remove any leading or trailing whitespace from processed
template fields.
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
### Conclusion
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.

701
docs/_static/alabaster.css vendored Normal file
View File

@@ -0,0 +1,701 @@
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Georgia, serif;
font-size: 17px;
background-color: #fff;
color: #000;
margin: 0;
padding: 0;
}
div.document {
width: 940px;
margin: 30px auto 0 auto;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 220px;
}
div.sphinxsidebar {
width: 220px;
font-size: 14px;
line-height: 1.5;
}
hr {
border: 1px solid #B1B4B6;
}
div.body {
background-color: #fff;
color: #3E4349;
padding: 0 30px 0 30px;
}
div.body > .section {
text-align: left;
}
div.footer {
width: 940px;
margin: 20px auto 30px auto;
font-size: 14px;
color: #888;
text-align: right;
}
div.footer a {
color: #888;
}
p.caption {
font-family: inherit;
font-size: inherit;
}
div.relations {
display: none;
}
div.sphinxsidebar a {
color: #444;
text-decoration: none;
border-bottom: 1px dotted #999;
}
div.sphinxsidebar a:hover {
border-bottom: 1px solid #999;
}
div.sphinxsidebarwrapper {
padding: 18px 10px;
}
div.sphinxsidebarwrapper p.logo {
padding: 0;
margin: -10px 0 0 0px;
text-align: center;
}
div.sphinxsidebarwrapper h1.logo {
margin-top: -10px;
text-align: center;
margin-bottom: 5px;
text-align: left;
}
div.sphinxsidebarwrapper h1.logo-name {
margin-top: 0px;
}
div.sphinxsidebarwrapper p.blurb {
margin-top: 0;
font-style: normal;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Georgia, serif;
color: #444;
font-size: 24px;
font-weight: normal;
margin: 0 0 5px 0;
padding: 0;
}
div.sphinxsidebar h4 {
font-size: 20px;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p.logo a,
div.sphinxsidebar h3 a,
div.sphinxsidebar p.logo a:hover,
div.sphinxsidebar h3 a:hover {
border: none;
}
div.sphinxsidebar p {
color: #555;
margin: 10px 0;
}
div.sphinxsidebar ul {
margin: 10px 0;
padding: 0;
color: #000;
}
div.sphinxsidebar ul li.toctree-l1 > a {
font-size: 120%;
}
div.sphinxsidebar ul li.toctree-l2 > a {
font-size: 110%;
}
div.sphinxsidebar input {
border: 1px solid #CCC;
font-family: Georgia, serif;
font-size: 1em;
}
div.sphinxsidebar hr {
border: none;
height: 1px;
color: #AAA;
background: #AAA;
text-align: left;
margin-left: 0;
width: 50%;
}
div.sphinxsidebar .badge {
border-bottom: none;
}
div.sphinxsidebar .badge:hover {
border-bottom: none;
}
/* To address an issue with donation coming after search */
div.sphinxsidebar h3.donation {
margin-top: 10px;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #004B6B;
text-decoration: underline;
}
a:hover {
color: #6D4100;
text-decoration: underline;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: Georgia, serif;
font-weight: normal;
margin: 30px 0px 10px 0px;
padding: 0;
}
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
div.body h2 { font-size: 180%; }
div.body h3 { font-size: 150%; }
div.body h4 { font-size: 130%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #DDD;
padding: 0 4px;
text-decoration: none;
}
a.headerlink:hover {
color: #444;
background: #EAEAEA;
}
div.body p, div.body dd, div.body li {
line-height: 1.4em;
}
div.admonition {
margin: 20px 0px;
padding: 10px 30px;
background-color: #EEE;
border: 1px solid #CCC;
}
div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
background-color: #FBFBFB;
border-bottom: 1px solid #fafafa;
}
div.admonition p.admonition-title {
font-family: Georgia, serif;
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
}
div.admonition p.last {
margin-bottom: 0;
}
div.highlight {
background-color: #fff;
}
dt:target, .highlight {
background: #FAF3E8;
}
div.warning {
background-color: #FCC;
border: 1px solid #FAA;
}
div.danger {
background-color: #FCC;
border: 1px solid #FAA;
-moz-box-shadow: 2px 2px 4px #D52C2C;
-webkit-box-shadow: 2px 2px 4px #D52C2C;
box-shadow: 2px 2px 4px #D52C2C;
}
div.error {
background-color: #FCC;
border: 1px solid #FAA;
-moz-box-shadow: 2px 2px 4px #D52C2C;
-webkit-box-shadow: 2px 2px 4px #D52C2C;
box-shadow: 2px 2px 4px #D52C2C;
}
div.caution {
background-color: #FCC;
border: 1px solid #FAA;
}
div.attention {
background-color: #FCC;
border: 1px solid #FAA;
}
div.important {
background-color: #EEE;
border: 1px solid #CCC;
}
div.note {
background-color: #EEE;
border: 1px solid #CCC;
}
div.tip {
background-color: #EEE;
border: 1px solid #CCC;
}
div.hint {
background-color: #EEE;
border: 1px solid #CCC;
}
div.seealso {
background-color: #EEE;
border: 1px solid #CCC;
}
div.topic {
background-color: #EEE;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre, tt, code {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.9em;
}
.hll {
background-color: #FFC;
margin: 0 -12px;
padding: 0 12px;
display: block;
}
img.screenshot {
}
tt.descname, tt.descclassname, code.descname, code.descclassname {
font-size: 0.95em;
}
tt.descname, code.descname {
padding-right: 0.08em;
}
img.screenshot {
-moz-box-shadow: 2px 2px 4px #EEE;
-webkit-box-shadow: 2px 2px 4px #EEE;
box-shadow: 2px 2px 4px #EEE;
}
table.docutils {
border: 1px solid #888;
-moz-box-shadow: 2px 2px 4px #EEE;
-webkit-box-shadow: 2px 2px 4px #EEE;
box-shadow: 2px 2px 4px #EEE;
}
table.docutils td, table.docutils th {
border: 1px solid #888;
padding: 0.25em 0.7em;
}
table.field-list, table.footnote {
border: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
table.footnote {
margin: 15px 0;
width: 100%;
border: 1px solid #EEE;
background: #FDFDFD;
font-size: 0.9em;
}
table.footnote + table.footnote {
margin-top: -15px;
border-top: none;
}
table.field-list th {
padding: 0 0.8em 0 0;
}
table.field-list td {
padding: 0;
}
table.field-list p {
margin-bottom: 0.8em;
}
/* Cloned from
* https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
*/
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
table.footnote td.label {
width: .1px;
padding: 0.3em 0 0.3em 0.5em;
}
table.footnote td {
padding: 0.3em 0.5em;
}
dl {
margin: 0;
padding: 0;
}
dl dd {
margin-left: 30px;
}
blockquote {
margin: 0 0 0 30px;
padding: 0;
}
ul, ol {
/* Matches the 30px from the narrow-screen "li > ul" selector below */
margin: 10px 0 10px 30px;
padding: 0;
}
pre {
background: #EEE;
padding: 7px 30px;
margin: 15px 0px;
line-height: 1.3em;
}
div.viewcode-block:target {
background: #ffd;
}
dl pre, blockquote pre, li pre {
margin-left: 0;
padding-left: 30px;
}
tt, code {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
}
tt.xref, code.xref, a tt {
background-color: #FBFBFB;
border-bottom: 1px solid #fff;
}
a.reference {
text-decoration: none;
border-bottom: 1px dotted #004B6B;
}
/* Don't put an underline on images */
a.image-reference, a.image-reference:hover {
border-bottom: none;
}
a.reference:hover {
border-bottom: 1px solid #6D4100;
}
a.footnote-reference {
text-decoration: none;
font-size: 0.7em;
vertical-align: top;
border-bottom: 1px dotted #004B6B;
}
a.footnote-reference:hover {
border-bottom: 1px solid #6D4100;
}
a:hover tt, a:hover code {
background: #EEE;
}
@media screen and (max-width: 870px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100%;
}
div.documentwrapper {
margin-left: 0;
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
}
div.bodywrapper {
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
ul {
margin-left: 0;
}
li > ul {
/* Matches the 30px from the "ul, ol" selector above */
margin-left: 30px;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.bodywrapper {
margin: 0;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
@media screen and (max-width: 875px) {
body {
margin: 0;
padding: 20px 30px;
}
div.documentwrapper {
float: none;
background: #fff;
}
div.sphinxsidebar {
display: block;
float: none;
width: 102.5%;
margin: 50px -30px -20px -30px;
padding: 10px 20px;
background: #333;
color: #FFF;
}
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
div.sphinxsidebar h3 a {
color: #fff;
}
div.sphinxsidebar a {
color: #AAA;
}
div.sphinxsidebar p.logo {
display: none;
}
div.document {
width: 100%;
margin: 0;
}
div.footer {
display: none;
}
div.bodywrapper {
margin: 0;
}
div.body {
min-height: 0;
padding: 0;
}
.rtd_doc_footer {
display: none;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
/* misc. */
.revsys-inline {
display: none!important;
}
/* Make nested-list/multi-paragraph items look better in Releases changelog
* pages. Without this, docutils' magical list fuckery causes inconsistent
* formatting between different release sub-lists.
*/
div#changelog > div.section > ul > li > p:only-child {
margin-bottom: 0;
}
/* Hide fugly table cell borders in ..bibliography:: directive output */
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
border: none;
/* Below needed in some edge cases; if not applied, bottom shadows appear */
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
/* relbar */
.related {
line-height: 30px;
width: 100%;
font-size: 0.9rem;
}
.related.top {
border-bottom: 1px solid #EEE;
margin-bottom: 20px;
}
.related.bottom {
border-top: 1px solid #EEE;
}
.related ul {
padding: 0;
margin: 0;
list-style: none;
}
.related li {
display: inline;
}
nav#rellinks {
float: right;
}
nav#rellinks li+li:before {
content: "|";
}
nav#breadcrumbs li+li:before {
content: "\00BB";
}
/* Hide certain items when printing */
@media print {
div.related {
display: none;
}
}

905
docs/_static/basic.css vendored Normal file
View File

@@ -0,0 +1,905 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li p.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 450px;
max-width: 800px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
a.headerlink {
visibility: hidden;
}
a.brackets:before,
span.brackets > a:before{
content: "[";
}
a.brackets:after,
span.brackets > a:after {
content: "]";
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, figure.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, figure.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, figure.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, figure.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-default {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar,
aside.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
clear: right;
overflow-x: auto;
}
p.sidebar-title {
font-weight: bold;
}
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
aside.sidebar > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
aside.sidebar::after,
div.topic::after,
div.admonition::after,
blockquote::after {
display: block;
content: '';
clear: both;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
table.align-default {
margin-left: auto;
margin-right: auto;
}
table caption span.caption-number {
font-style: italic;
}
table caption span.caption-text {
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure, figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption, figcaption {
padding: 0.3em;
}
div.figure p.caption span.caption-number,
figcaption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text,
figcaption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
table.field-list td, table.field-list th {
border: 0 !important;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
/* -- object description styles --------------------------------------------- */
.sig {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
}
.sig-name, code.descname {
background-color: transparent;
font-weight: bold;
}
.sig-name {
font-size: 1.1em;
}
code.descname {
font-size: 1.2em;
}
.sig-prename, code.descclassname {
background-color: transparent;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.sig-param.n {
font-style: italic;
}
/* C++ specific styling */
.sig-inline.c-texpr,
.sig-inline.cpp-texpr {
font-family: unset;
}
.sig.c .k, .sig.c .kt,
.sig.cpp .k, .sig.cpp .kt {
color: #0033B3;
}
.sig.c .m,
.sig.cpp .m {
color: #1750EB;
}
.sig.c .s, .sig.c .sc,
.sig.cpp .s, .sig.cpp .sc {
color: #067D17;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
:not(li) > ol > li:first-child > :first-child,
:not(li) > ul > li:first-child > :first-child {
margin-top: 0px;
}
:not(li) > ol > li:last-child > :last-child,
:not(li) > ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol.simple ol p,
ol.simple ul p,
ul.simple ol p,
ul.simple ul p {
margin-top: 0;
}
ol.simple > li:not(:first-child) > p,
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ol.simple p,
ul.simple p {
margin-bottom: 0;
}
dl.footnote > dt,
dl.citation > dt {
float: left;
margin-right: 0.5em;
}
dl.footnote > dd,
dl.citation > dd {
margin-bottom: 0em;
}
dl.footnote > dd:after,
dl.citation > dd:after {
content: "";
clear: both;
}
dl.field-list {
display: grid;
grid-template-columns: fit-content(30%) auto;
}
dl.field-list > dt {
font-weight: bold;
word-break: break-word;
padding-left: 0.5em;
padding-right: 5px;
}
dl.field-list > dt:after {
content: ":";
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd > :first-child {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
rect.highlighted {
fill: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
.classifier:before {
font-style: normal;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
margin: 1em 0;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

1
docs/_static/custom.css vendored Normal file
View File

@@ -0,0 +1 @@
/* This file intentionally left blank. */

323
docs/_static/doctools.js vendored Normal file
View File

@@ -0,0 +1,323 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Sphinx JavaScript utilities for all documentation.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
/**
* make the code below compatible with browsers without
* an installed firebug like debugger
if (!window.console || !console.firebug) {
var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
"dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
"profile", "profileEnd"];
window.console = {};
for (var i = 0; i < names.length; ++i)
window.console[names[i]] = function() {};
}
*/
/**
* small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/
jQuery.urldecode = function(x) {
if (!x) {
return x
}
return decodeURIComponent(x.replace(/\+/g, ' '));
};
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s === 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
};
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node, addItems) {
if (node.nodeType === 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 &&
!jQuery(node.parentNode).hasClass(className) &&
!jQuery(node.parentNode).hasClass("nohighlight")) {
var span;
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.className = className;
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
var bbox = node.parentElement.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute('class', className);
addItems.push({
"parent": node.parentNode,
"target": rect});
}
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this, addItems);
});
}
}
var addItems = [];
var result = this.each(function() {
highlight(this, addItems);
});
for (var i = 0; i < addItems.length; ++i) {
jQuery(addItems[i].parent).before(addItems[i].target);
}
return result;
};
/*
* backward compatibility for jQuery.browser
* This will be supported until firefox bug is fixed.
*/
if (!jQuery.browser) {
jQuery.uaMatch = function(ua) {
ua = ua.toLowerCase();
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[];
return {
browser: match[ 1 ] || "",
version: match[ 2 ] || "0"
};
};
jQuery.browser = {};
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
}
/**
* Small JavaScript module for the documentation.
*/
var Documentation = {
init : function() {
this.fixFirefoxAnchorBug();
this.highlightSearchWords();
this.initIndexTable();
if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
this.initOnKeyListeners();
}
},
/**
* i18n support
*/
TRANSLATIONS : {},
PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
LOCALE : 'unknown',
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
gettext : function(string) {
var translated = Documentation.TRANSLATIONS[string];
if (typeof translated === 'undefined')
return string;
return (typeof translated === 'string') ? translated : translated[0];
},
ngettext : function(singular, plural, n) {
var translated = Documentation.TRANSLATIONS[singular];
if (typeof translated === 'undefined')
return (n == 1) ? singular : plural;
return translated[Documentation.PLURALEXPR(n)];
},
addTranslations : function(catalog) {
for (var key in catalog.messages)
this.TRANSLATIONS[key] = catalog.messages[key];
this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
this.LOCALE = catalog.locale;
},
/**
* add context elements like header anchor links
*/
addContextElements : function() {
$('div[id] > :header:first').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this headline')).
appendTo(this);
});
$('dt[id]').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this definition')).
appendTo(this);
});
},
/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash && $.browser.mozilla)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},
/**
* highlight the search words provided in the url in the text
*/
highlightSearchWords : function() {
var params = $.getQueryParameters();
var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
if (terms.length) {
var body = $('div.body');
if (!body.length) {
body = $('body');
}
window.setTimeout(function() {
$.each(terms, function() {
body.highlightText(this.toLowerCase(), 'highlighted');
});
}, 10);
$('<p class="highlight-link"><a href="javascript:Documentation.' +
'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
.appendTo($('#searchbox'));
}
},
/**
* init the domain index toggle buttons
*/
initIndexTable : function() {
var togglers = $('img.toggler').click(function() {
var src = $(this).attr('src');
var idnum = $(this).attr('id').substr(7);
$('tr.cg-' + idnum).toggle();
if (src.substr(-9) === 'minus.png')
$(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
else
$(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
}).css('display', '');
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
togglers.click();
}
},
/**
* helper function to hide the search marks again
*/
hideSearchWords : function() {
$('#searchbox .highlight-link').fadeOut(300);
$('span.highlighted').removeClass('highlighted');
},
/**
* make the url absolute
*/
makeURL : function(relativeURL) {
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
},
/**
* get the current relative url
*/
getCurrentURL : function() {
var path = document.location.pathname;
var parts = path.split(/\//);
$.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
if (this === '..')
parts.pop();
});
var url = parts.join('/');
return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
},
initOnKeyListeners: function() {
$(document).keydown(function(event) {
var activeElementType = document.activeElement.tagName;
// don't navigate when in search box, textarea, dropdown or button
if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
&& activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
&& !event.shiftKey) {
switch (event.keyCode) {
case 37: // left
var prevHref = $('link[rel="prev"]').prop('href');
if (prevHref) {
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;
}
}
});
}
};
// quick alias for translations
_ = Documentation.gettext;
$(document).ready(function() {
Documentation.init();
});

12
docs/_static/documentation_options.js vendored Normal file
View File

@@ -0,0 +1,12 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.44.8',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
NAVIGATION_WITH_KEYS: false
};

BIN
docs/_static/file.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

10872
docs/_static/jquery-3.5.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
docs/_static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

297
docs/_static/language_data.js vendored Normal file
View File

@@ -0,0 +1,297 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
/* Non-minified version is copied as a separate JS file, is available */
/**
* Porter Stemmer
*/
var Stemmer = function() {
var step2list = {
ational: 'ate',
tional: 'tion',
enci: 'ence',
anci: 'ance',
izer: 'ize',
bli: 'ble',
alli: 'al',
entli: 'ent',
eli: 'e',
ousli: 'ous',
ization: 'ize',
ation: 'ate',
ator: 'ate',
alism: 'al',
iveness: 'ive',
fulness: 'ful',
ousness: 'ous',
aliti: 'al',
iviti: 'ive',
biliti: 'ble',
logi: 'log'
};
var step3list = {
icate: 'ic',
ative: '',
alize: 'al',
iciti: 'ic',
ical: 'ic',
ful: '',
ness: ''
};
var c = "[^aeiou]"; // consonant
var v = "[aeiouy]"; // vowel
var C = c + "[^aeiouy]*"; // consonant sequence
var V = v + "[aeiou]*"; // vowel sequence
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
var s_v = "^(" + C + ")?" + v; // vowel in stem
this.stemWord = function (w) {
var stem;
var suffix;
var firstch;
var origword = w;
if (w.length < 3)
return w;
var re;
var re2;
var re3;
var re4;
firstch = w.substr(0,1);
if (firstch == "y")
w = firstch.toUpperCase() + w.substr(1);
// Step 1a
re = /^(.+?)(ss|i)es$/;
re2 = /^(.+?)([^s])s$/;
if (re.test(w))
w = w.replace(re,"$1$2");
else if (re2.test(w))
w = w.replace(re2,"$1$2");
// Step 1b
re = /^(.+?)eed$/;
re2 = /^(.+?)(ed|ing)$/;
if (re.test(w)) {
var fp = re.exec(w);
re = new RegExp(mgr0);
if (re.test(fp[1])) {
re = /.$/;
w = w.replace(re,"");
}
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1];
re2 = new RegExp(s_v);
if (re2.test(stem)) {
w = stem;
re2 = /(at|bl|iz)$/;
re3 = new RegExp("([^aeiouylsz])\\1$");
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re2.test(w))
w = w + "e";
else if (re3.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
else if (re4.test(w))
w = w + "e";
}
}
// Step 1c
re = /^(.+?)y$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(s_v);
if (re.test(stem))
w = stem + "i";
}
// Step 2
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step2list[suffix];
}
// Step 3
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step3list[suffix];
}
// Step 4
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
re2 = /^(.+?)(s|t)(ion)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
if (re.test(stem))
w = stem;
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1] + fp[2];
re2 = new RegExp(mgr1);
if (re2.test(stem))
w = stem;
}
// Step 5
re = /^(.+?)e$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
re2 = new RegExp(meq1);
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
w = stem;
}
re = /ll$/;
re2 = new RegExp(mgr1);
if (re.test(w) && re2.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
// and turn initial Y back to y
if (firstch == "y")
w = firstch.toLowerCase() + w.substr(1);
return w;
}
}
var splitChars = (function() {
var result = {};
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,
2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,
2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,
3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,
3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,
4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,
8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,
11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,
43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];
var i, j, start, end;
for (i = 0; i < singles.length; i++) {
result[singles[i]] = true;
}
var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],
[722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],
[1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],
[1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],
[1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],
[2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],
[2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],
[2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],
[2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],
[2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],
[2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],
[2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],
[3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],
[3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],
[3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],
[3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],
[3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],
[3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],
[4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],
[4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],
[4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],
[4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],
[5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],
[6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],
[6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],
[6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],
[6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],
[7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],
[7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],
[8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],
[8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],
[8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],
[10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],
[11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],
[12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],
[12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],
[12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],
[19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],
[42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],
[42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],
[43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],
[43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],
[43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],
[43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],
[44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],
[57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],
[64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],
[65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],
[65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];
for (i = 0; i < ranges.length; i++) {
start = ranges[i][0];
end = ranges[i][1];
for (j = start; j <= end; j++) {
result[j] = true;
}
}
return result;
})();
function splitQuery(query) {
var result = [];
var start = -1;
for (var i = 0; i < query.length; i++) {
if (splitChars[query.charCodeAt(i)]) {
if (start !== -1) {
result.push(query.slice(start, i));
start = -1;
}
} else if (start === -1) {
start = i;
}
}
if (start !== -1) {
result.push(query.slice(start));
}
return result;
}

BIN
docs/_static/minus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
docs/_static/plus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

82
docs/_static/pygments.css vendored Normal file
View File

@@ -0,0 +1,82 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
.highlight .g { color: #000000 } /* Generic */
.highlight .k { color: #004461; font-weight: bold } /* Keyword */
.highlight .l { color: #000000 } /* Literal */
.highlight .n { color: #000000 } /* Name */
.highlight .o { color: #582800 } /* Operator */
.highlight .x { color: #000000 } /* Other */
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #8f5902 } /* Comment.Preproc */
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
.highlight .gd { color: #a40000 } /* Generic.Deleted */
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
.highlight .gr { color: #ef2929 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #745334 } /* Generic.Prompt */
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */
.highlight .ld { color: #000000 } /* Literal.Date */
.highlight .m { color: #990000 } /* Literal.Number */
.highlight .s { color: #4e9a06 } /* Literal.String */
.highlight .na { color: #c4a000 } /* Name.Attribute */
.highlight .nb { color: #004461 } /* Name.Builtin */
.highlight .nc { color: #000000 } /* Name.Class */
.highlight .no { color: #000000 } /* Name.Constant */
.highlight .nd { color: #888888 } /* Name.Decorator */
.highlight .ni { color: #ce5c00 } /* Name.Entity */
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #000000 } /* Name.Function */
.highlight .nl { color: #f57900 } /* Name.Label */
.highlight .nn { color: #000000 } /* Name.Namespace */
.highlight .nx { color: #000000 } /* Name.Other */
.highlight .py { color: #000000 } /* Name.Property */
.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #000000 } /* Name.Variable */
.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */
.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */
.highlight .mb { color: #990000 } /* Literal.Number.Bin */
.highlight .mf { color: #990000 } /* Literal.Number.Float */
.highlight .mh { color: #990000 } /* Literal.Number.Hex */
.highlight .mi { color: #990000 } /* Literal.Number.Integer */
.highlight .mo { color: #990000 } /* Literal.Number.Oct */
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #000000 } /* Name.Function.Magic */
.highlight .vc { color: #000000 } /* Name.Variable.Class */
.highlight .vg { color: #000000 } /* Name.Variable.Global */
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */

529
docs/_static/searchtools.js vendored Normal file
View File

@@ -0,0 +1,529 @@
/*
* searchtools.js
* ~~~~~~~~~~~~~~~~
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
if (!Scorer) {
/**
* Simple result scoring code.
*/
var Scorer = {
// Implement the following function to further tweak the score for each result
// The function takes a result array [filename, title, anchor, descr, score]
// and returns the new score.
/*
score: function(result) {
return result[4];
},
*/
// query matches the full name of an object
objNameMatch: 11,
// or matches in the last dotted part of the object name
objPartialMatch: 6,
// Additive scores depending on the priority of the object
objPrio: {0: 15, // used to be importantResults
1: 5, // used to be objectResults
2: -5}, // used to be unimportantResults
// Used when the priority is not in the mapping.
objPrioDefault: 0,
// query found in title
title: 15,
partialTitle: 7,
// query found in terms
term: 5,
partialTerm: 2
};
}
if (!splitQuery) {
function splitQuery(query) {
return query.split(/\s+/);
}
}
/**
* Search Module
*/
var Search = {
_index : null,
_queued_query : null,
_pulse_status : -1,
htmlToText : function(htmlString) {
var virtualDocument = document.implementation.createHTMLDocument('virtual');
var htmlElement = $(htmlString, virtualDocument);
htmlElement.find('.headerlink').remove();
docContent = htmlElement.find('[role=main]')[0];
if(docContent === undefined) {
console.warn("Content block not found. Sphinx search tries to obtain it " +
"via '[role=main]'. Could you check your theme or template.");
return "";
}
return docContent.textContent || docContent.innerText;
},
init : function() {
var params = $.getQueryParameters();
if (params.q) {
var query = params.q[0];
$('input[name="q"]')[0].value = query;
this.performSearch(query);
}
},
loadIndex : function(url) {
$.ajax({type: "GET", url: url, data: null,
dataType: "script", cache: true,
complete: function(jqxhr, textstatus) {
if (textstatus != "success") {
document.getElementById("searchindexloader").src = url;
}
}});
},
setIndex : function(index) {
var q;
this._index = index;
if ((q = this._queued_query) !== null) {
this._queued_query = null;
Search.query(q);
}
},
hasIndex : function() {
return this._index !== null;
},
deferQuery : function(query) {
this._queued_query = query;
},
stopPulse : function() {
this._pulse_status = 0;
},
startPulse : function() {
if (this._pulse_status >= 0)
return;
function pulse() {
var i;
Search._pulse_status = (Search._pulse_status + 1) % 4;
var dotString = '';
for (i = 0; i < Search._pulse_status; i++)
dotString += '.';
Search.dots.text(dotString);
if (Search._pulse_status > -1)
window.setTimeout(pulse, 500);
}
pulse();
},
/**
* perform a search for something (or wait until index is loaded)
*/
performSearch : function(query) {
// create the required interface elements
this.out = $('#search-results');
this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
this.dots = $('<span></span>').appendTo(this.title);
this.status = $('<p class="search-summary">&nbsp;</p>').appendTo(this.out);
this.output = $('<ul class="search"/>').appendTo(this.out);
$('#search-progress').text(_('Preparing search...'));
this.startPulse();
// index already loaded, the browser was quick!
if (this.hasIndex())
this.query(query);
else
this.deferQuery(query);
},
/**
* execute search (requires search index to be loaded)
*/
query : function(query) {
var i;
// stem the searchterms and add them to the correct list
var stemmer = new Stemmer();
var searchterms = [];
var excluded = [];
var hlterms = [];
var tmp = splitQuery(query);
var objectterms = [];
for (i = 0; i < tmp.length; i++) {
if (tmp[i] !== "") {
objectterms.push(tmp[i].toLowerCase());
}
if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
// skip this "word"
continue;
}
// stem the word
var word = stemmer.stemWord(tmp[i].toLowerCase());
// prevent stemmer from cutting word smaller than two chars
if(word.length < 3 && tmp[i].length >= 3) {
word = tmp[i];
}
var toAppend;
// select the correct list
if (word[0] == '-') {
toAppend = excluded;
word = word.substr(1);
}
else {
toAppend = searchterms;
hlterms.push(tmp[i].toLowerCase());
}
// only add if not already in the list
if (!$u.contains(toAppend, word))
toAppend.push(word);
}
var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
// console.debug('SEARCH: searching for:');
// console.info('required: ', searchterms);
// console.info('excluded: ', excluded);
// prepare search
var terms = this._index.terms;
var titleterms = this._index.titleterms;
// array of [filename, title, anchor, descr, score]
var results = [];
$('#search-progress').empty();
// lookup as object
for (i = 0; i < objectterms.length; i++) {
var others = [].concat(objectterms.slice(0, i),
objectterms.slice(i+1, objectterms.length));
results = results.concat(this.performObjectSearch(objectterms[i], others));
}
// lookup as search terms in fulltext
results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
// let the scorer override scores with a custom scoring function
if (Scorer.score) {
for (i = 0; i < results.length; i++)
results[i][4] = Scorer.score(results[i]);
}
// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
results.sort(function(a, b) {
var left = a[4];
var right = b[4];
if (left > right) {
return 1;
} else if (left < right) {
return -1;
} else {
// same score: sort alphabetically
left = a[1].toLowerCase();
right = b[1].toLowerCase();
return (left > right) ? -1 : ((left < right) ? 1 : 0);
}
});
// for debugging
//Search.lastresults = results.slice(); // a copy
//console.info('search results:', Search.lastresults);
// print the results
var resultCount = results.length;
function displayNextItem() {
// results left, load the summary and display it
if (results.length) {
var item = results.pop();
var listItem = $('<li></li>');
var requestUrl = "";
var linkUrl = "";
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
// dirhtml builder
var dirname = item[0] + '/';
if (dirname.match(/\/index\/$/)) {
dirname = dirname.substring(0, dirname.length-6);
} else if (dirname == 'index/') {
dirname = '';
}
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname;
linkUrl = requestUrl;
} else {
// normal html builders
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX;
linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX;
}
listItem.append($('<a/>').attr('href',
linkUrl +
highlightstring + item[2]).html(item[1]));
if (item[3]) {
listItem.append($('<span> (' + item[3] + ')</span>'));
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
$.ajax({url: requestUrl,
dataType: "text",
complete: function(jqxhr, textstatus) {
var data = jqxhr.responseText;
if (data !== '' && data !== undefined) {
var summary = Search.makeSearchSummary(data, searchterms, hlterms);
if (summary) {
listItem.append(summary);
}
}
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
}});
} else {
// no source available, just display title
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
}
}
// search finished, update title and status message
else {
Search.stopPulse();
Search.title.text(_('Search Results'));
if (!resultCount)
Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
else
Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
Search.status.fadeIn(500);
}
}
displayNextItem();
},
/**
* search for object names
*/
performObjectSearch : function(object, otherterms) {
var filenames = this._index.filenames;
var docnames = this._index.docnames;
var objects = this._index.objects;
var objnames = this._index.objnames;
var titles = this._index.titles;
var i;
var results = [];
for (var prefix in objects) {
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) {
var score = 0;
var parts = fullnameLower.split('.');
// check for different match types: exact matches of full name or
// "last name" (i.e. last dotted part)
if (fullnameLower == object || parts[parts.length - 1] == object) {
score += Scorer.objNameMatch;
// matches in last name
} else if (parts[parts.length - 1].indexOf(object) > -1) {
score += Scorer.objPartialMatch;
}
var objname = objnames[match[1]][2];
var title = titles[match[0]];
// If more than one term searched for, we require other words to be
// found in the name/title/description
if (otherterms.length > 0) {
var haystack = (prefix + ' ' + name + ' ' +
objname + ' ' + title).toLowerCase();
var allfound = true;
for (i = 0; i < otherterms.length; i++) {
if (haystack.indexOf(otherterms[i]) == -1) {
allfound = false;
break;
}
}
if (!allfound) {
continue;
}
}
var descr = objname + _(', in ') + title;
var anchor = match[3];
if (anchor === '')
anchor = fullname;
else if (anchor == '-')
anchor = objnames[match[1]][1] + '-' + fullname;
// add custom score for some objects according to scorer
if (Scorer.objPrio.hasOwnProperty(match[2])) {
score += Scorer.objPrio[match[2]];
} else {
score += Scorer.objPrioDefault;
}
results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
}
}
}
return results;
},
/**
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
escapeRegExp : function(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
},
/**
* search for full-text terms in the index
*/
performTermsSearch : function(searchterms, excluded, terms, titleterms) {
var docnames = this._index.docnames;
var filenames = this._index.filenames;
var titles = this._index.titles;
var i, j, file;
var fileMap = {};
var scoreMap = {};
var results = [];
// perform the search on the required terms
for (i = 0; i < searchterms.length; i++) {
var word = searchterms[i];
var files = [];
var _o = [
{files: terms[word], score: Scorer.term},
{files: titleterms[word], score: Scorer.title}
];
// add support for partial matches
if (word.length > 2) {
var word_regex = this.escapeRegExp(word);
for (var w in terms) {
if (w.match(word_regex) && !terms[word]) {
_o.push({files: terms[w], score: Scorer.partialTerm})
}
}
for (var w in titleterms) {
if (w.match(word_regex) && !titleterms[word]) {
_o.push({files: titleterms[w], score: Scorer.partialTitle})
}
}
}
// no match but word was a required one
if ($u.every(_o, function(o){return o.files === undefined;})) {
break;
}
// found search word in contents
$u.each(_o, function(o) {
var _files = o.files;
if (_files === undefined)
return
if (_files.length === undefined)
_files = [_files];
files = files.concat(_files);
// set score for the word in each file to Scorer.term
for (j = 0; j < _files.length; j++) {
file = _files[j];
if (!(file in scoreMap))
scoreMap[file] = {};
scoreMap[file][word] = o.score;
}
});
// create the mapping
for (j = 0; j < files.length; j++) {
file = files[j];
if (file in fileMap && fileMap[file].indexOf(word) === -1)
fileMap[file].push(word);
else
fileMap[file] = [word];
}
}
// now check if the files don't contain excluded terms
for (file in fileMap) {
var valid = true;
// check if all requirements are matched
var filteredTermCount = // as search terms with length < 3 are discarded: ignore
searchterms.filter(function(term){return term.length > 2}).length
if (
fileMap[file].length != searchterms.length &&
fileMap[file].length != filteredTermCount
) continue;
// ensure that none of the excluded terms is in the search result
for (i = 0; i < excluded.length; i++) {
if (terms[excluded[i]] == file ||
titleterms[excluded[i]] == file ||
$u.contains(terms[excluded[i]] || [], file) ||
$u.contains(titleterms[excluded[i]] || [], file)) {
valid = false;
break;
}
}
// if we have still a valid result we can add it to the result list
if (valid) {
// select one (max) score for the file.
// for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
}
}
return results;
},
/**
* helper function to return a node containing the
* search summary for a given text. keywords is a list
* of stemmed words, hlwords is the list of normal, unstemmed
* words. the first one is used to find the occurrence, the
* latter for highlighting it.
*/
makeSearchSummary : function(htmlText, keywords, hlwords) {
var text = Search.htmlToText(htmlText);
if (text == "") {
return null;
}
var textLower = text.toLowerCase();
var start = 0;
$.each(keywords, function() {
var i = textLower.indexOf(this.toLowerCase());
if (i > -1)
start = i;
});
start = Math.max(start - 120, 0);
var excerpt = ((start > 0) ? '...' : '') +
$.trim(text.substr(start, 240)) +
((start + 240 - text.length) ? '...' : '');
var rv = $('<p class="context"></p>').text(excerpt);
$.each(hlwords, function() {
rv = rv.highlightText(this, 'highlighted');
});
return rv;
}
};
$(document).ready(function() {
Search.init();
});

2027
docs/_static/underscore-1.12.0.js vendored Normal file

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

999
docs/_static/underscore-1.3.1.js vendored Normal file
View File

@@ -0,0 +1,999 @@
// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier,
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root['_'] = _;
}
// Current version.
_.VERSION = '1.3.1';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
if (obj.length === +obj.length) results.length = obj.length;
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = _.toArray(obj).reverse();
if (context && !initial) iterator = _.bind(iterator, context);
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
found = any(obj, function(value) {
return value === target;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Shuffle an array.
_.shuffle = function(obj) {
var shuffled = [], rand;
each(obj, function(value, index, list) {
if (index == 0) {
shuffled[0] = value;
} else {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
}
});
return shuffled;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, val) {
var result = {};
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the last entry of the array. Especcialy useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with
// `_.map`.
_.initial = function(array, n, guard) {
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array, shallow) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator) {
var initial = iterator ? _.map(array, iterator) : array;
var result = [];
_.reduce(initial, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
memo[memo.length] = el;
result[result.length] = array[i];
}
return memo;
}, []);
return result;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments, true));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
var rest = _.flatten(slice.call(arguments, 1));
return _.filter(array, function(value){ return !_.include(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (i in array && array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function bind(func, context) {
var bound, args;
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, throttling, more;
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
return function() {
context = this; args = arguments;
var later = function() {
timeout = null;
if (more) func.apply(context, args);
whenDone();
};
if (!timeout) timeout = setTimeout(later, wait);
if (throttling) {
more = true;
} else {
func.apply(context, args);
}
whenDone();
throttling = true;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments, 0));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// Invoke a custom `isEqual` method if one is provided.
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = stack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (stack[length] == a) return true;
}
// Add the first object to the stack of traversed objects.
stack.push(a);
var size = 0, result = true;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
// Ensure commutative equality for sparse arrays.
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
}
}
} else {
// Objects with different constructors are not equivalent.
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
stack.pop();
return result;
}
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, []);
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return toString.call(obj) == '[object Arguments]';
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && _.has(obj, 'callee'));
};
}
// Is a given value a function?
_.isFunction = function(obj) {
return toString.call(obj) == '[object Function]';
};
// Is a given value a string?
_.isString = function(obj) {
return toString.call(obj) == '[object String]';
};
// Is a given value a number?
_.isNumber = function(obj) {
return toString.call(obj) == '[object Number]';
};
// Is the given value `NaN`?
_.isNaN = function(obj) {
// `NaN` is the only value for which `===` is not reflexive.
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};
// Is a given value a date?
_.isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Has own property?
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Escape a string for HTML interpolation.
_.escape = function(string) {
return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /.^/;
// Within an interpolation, evaluation, or escaping, remove HTML escaping
// that had been previously added.
var unescape = function(code) {
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.escape || noMatch, function(match, code) {
return "',_.escape(" + unescape(code) + "),'";
})
.replace(c.interpolate || noMatch, function(match, code) {
return "'," + unescape(code) + ",'";
})
.replace(c.evaluate || noMatch, function(match, code) {
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', '_', tmpl);
if (data) return func(data, _);
return function(data) {
return func.call(this, data, _);
};
};
// Add a "chain" function, which will delegate to the wrapper.
_.chain = function(obj) {
return _(obj).chain();
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
var wrapped = this._wrapped;
method.apply(wrapped, arguments);
var length = wrapped.length;
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
return result(wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
}).call(this);

6
docs/_static/underscore.js vendored Normal file

File diff suppressed because one or more lines are too long

109
docs/cli.html Normal file
View File

@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<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 command line interface (CLI) &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="osxphotos package" href="reference.html" />
<link rel="prev" title="Welcome to osxphotoss documentation!" href="index.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<section id="osxphotos-command-line-interface-cli">
<h1>osxphotos command line interface (CLI)<a class="headerlink" href="#osxphotos-command-line-interface-cli" title="Permalink to this headline"></a></h1>
</section>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul class="current">
<li class="toctree-l1 current"><a class="current reference internal" href="#">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
<li>Previous: <a href="index.html" title="previous chapter">Welcome to osxphotoss documentation!</a></li>
<li>Next: <a href="reference.html" title="next chapter">osxphotos package</a></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/cli.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

104
docs/genindex.html Normal file
View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="#" />
<link rel="search" title="Search" href="search.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1 id="index">Index</h1>
<div class="genindex-jumpbox">
</div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

370
docs/index.html Normal file
View File

@@ -0,0 +1,370 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="osxphotos command line interface (CLI)" href="cli.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<section id="welcome-to-osxphotos-s-documentation">
<h1>Welcome to osxphotoss documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline"></a></h1>
</section>
<section id="osxphotos">
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<section id="what-is-osxphotos">
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline"></a></h2>
<p>OSXPhotos provides both the ability to interact with and query Apples Photos.app library on macOS directly from your python code
as well as a very flexible command line interface (CLI) app for exporting photos.
You can query the Photos library database for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
You can also easily export both the original and edited photos.</p>
</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 &gt;= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
</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>
<section id="installation-using-pipx">
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline"></a></h3>
<p>If you arent familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
<ul class="simple">
<li><p>Open <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> (search for <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> in Spotlight or look in <code class="docutils literal notranslate"><span class="pre">Applications/Utilities</span></code>)</p></li>
<li><p>Install <code class="docutils literal notranslate"><span class="pre">homebrew</span></code> according to instructions at <a class="reference external" href="https://brew.sh/">https://brew.sh/</a></p></li>
<li><p>Type the following into Terminal: <code class="docutils literal notranslate"><span class="pre">brew</span> <span class="pre">install</span> <span class="pre">pipx</span></code></p></li>
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
</ul>
</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>
</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>
<span class="n">cd</span> <span class="n">osxphotos</span>
<span class="n">python3</span> <span class="n">setup</span><span class="o">.</span><span class="n">py</span> <span class="n">install</span>
</pre></div>
</div>
<p>I recommend you create a <a class="reference external" href="https://docs.python.org/3/tutorial/venv.html">virtual environment</a> before installing osxphotos.</p>
<p><strong>WARNING</strong> The git repo for this project is very large (&gt; 1GB) because it contains multiple Photos libraries used for testing
on different versions of macOS. If you just want to use the osxphotos package in your own code,
I recommend you install the latest version from <a class="reference external" href="https://pypi.org/project/osxphotos/">PyPI</a> which does not include all the test
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
If you arent comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
</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>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&gt;</span> <span class="n">osxphotos</span>
<span class="n">Usage</span><span class="p">:</span> <span class="n">osxphotos</span> <span class="p">[</span><span class="n">OPTIONS</span><span class="p">]</span> <span class="n">COMMAND</span> <span class="p">[</span><span class="n">ARGS</span><span class="p">]</span><span class="o">...</span>
<span class="n">Options</span><span class="p">:</span>
<span class="o">--</span><span class="n">db</span> <span class="o">&lt;</span><span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">&gt;</span> <span class="n">Specify</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">.</span> <span class="n">Path</span> <span class="n">to</span> <span class="n">Photos</span>
<span class="n">library</span><span class="o">/</span><span class="n">database</span> <span class="n">can</span> <span class="n">be</span> <span class="n">specified</span> <span class="n">using</span> <span class="n">either</span>
<span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">directly</span> <span class="k">as</span> <span class="n">PHOTOS_LIBRARY</span> <span class="n">positional</span>
<span class="n">argument</span><span class="o">.</span> <span class="n">If</span> <span class="n">neither</span> <span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">PHOTOS_LIBRARY</span>
<span class="n">provided</span><span class="p">,</span> <span class="n">will</span> <span class="n">attempt</span> <span class="n">to</span> <span class="n">find</span> <span class="n">the</span> <span class="n">library</span> <span class="n">to</span>
<span class="n">use</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">following</span> <span class="n">order</span><span class="p">:</span> <span class="mf">1.</span> <span class="n">last</span> <span class="n">opened</span>
<span class="n">library</span><span class="p">,</span> <span class="mf">2.</span> <span class="n">system</span> <span class="n">library</span><span class="p">,</span> <span class="mf">3.</span>
<span class="o">~/</span><span class="n">Pictures</span><span class="o">/</span><span class="n">Photos</span> <span class="n">Library</span><span class="o">.</span><span class="n">photoslibrary</span>
<span class="o">--</span><span class="n">json</span> <span class="n">Print</span> <span class="n">output</span> <span class="ow">in</span> <span class="n">JSON</span> <span class="nb">format</span><span class="o">.</span>
<span class="o">-</span><span class="n">v</span><span class="p">,</span> <span class="o">--</span><span class="n">version</span> <span class="n">Show</span> <span class="n">the</span> <span class="n">version</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
<span class="o">-</span><span class="n">h</span><span class="p">,</span> <span class="o">--</span><span class="n">help</span> <span class="n">Show</span> <span class="n">this</span> <span class="n">message</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
<span class="n">Commands</span><span class="p">:</span>
<span class="n">about</span> <span class="n">Print</span> <span class="n">information</span> <span class="n">about</span> <span class="n">osxphotos</span> <span class="n">including</span> <span class="n">license</span><span class="o">.</span>
<span class="n">albums</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">albums</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">dump</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="nb">all</span> <span class="n">photos</span> <span class="o">&amp;</span> <span class="n">associated</span> <span class="n">info</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span><span class="o">...</span>
<span class="n">export</span> <span class="n">Export</span> <span class="n">photos</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span> <span class="n">database</span><span class="o">.</span>
<span class="n">help</span> <span class="n">Print</span> <span class="n">help</span><span class="p">;</span> <span class="k">for</span> <span class="n">help</span> <span class="n">on</span> <span class="n">commands</span><span class="p">:</span> <span class="n">help</span> <span class="o">&lt;</span><span class="n">command</span><span class="o">&gt;.</span>
<span class="n">info</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">descriptive</span> <span class="n">info</span> <span class="n">of</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span> <span class="n">database</span><span class="o">.</span>
<span class="n">keywords</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">keywords</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">labels</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">image</span> <span class="n">classification</span> <span class="n">labels</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span><span class="o">...</span>
<span class="nb">list</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="n">Photos</span> <span class="n">libraries</span> <span class="n">found</span> <span class="n">on</span> <span class="n">the</span> <span class="n">system</span><span class="o">.</span>
<span class="n">persons</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">persons</span> <span class="p">(</span><span class="n">faces</span><span class="p">)</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">places</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">places</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">query</span> <span class="n">Query</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">using</span> <span class="mi">1</span> <span class="ow">or</span> <span class="n">more</span> <span class="n">search</span> <span class="n">options</span><span class="p">;</span> <span class="k">if</span><span class="o">...</span>
<span class="n">repl</span> <span class="n">Run</span> <span class="n">interactive</span> <span class="n">osxphotos</span> <span class="n">shell</span>
<span class="n">tutorial</span> <span class="n">Display</span> <span class="n">osxphotos</span> <span class="n">tutorial</span><span class="o">.</span>
</pre></div>
</div>
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">&lt;command_name&gt;</span></code></p>
<section id="command-line-examples">
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline"></a></h3>
<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>
</section>
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">&gt;results.json</span></code></p>
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
<h4>export photos to file structure based on 4-digit year and full name of month of photos creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{created.month}&quot;</span></code></p>
<p>(by default, it will attempt to use the system library)</p>
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
<h4>export photos to file structure based on 4-digit year of photos creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{media_type}&quot;</span></code></p>
</section>
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
<h4>export default library using country name/year as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{place.name.country,NoCountry}/{created.year}&quot;</span>&#160; <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
</section>
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">&quot;Big</span> <span class="pre">Videos&quot;</span></code></p>
</section>
</section>
</section>
<section id="example-uses-of-the-package">
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline"></a></h2>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Simple usage of the package &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">album_names</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords_as_dict</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons_as_dict</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">albums_as_dict</span><span class="p">)</span>
<span class="c1"># find all photos with Keyword = Foo and containing John Smith</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Foo&quot;</span><span class="p">],</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;John Smith&quot;</span><span class="p">])</span>
<span class="c1"># find all photos that include Alice Smith but do not contain the keyword Bar</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Alice Smith&quot;</span><span class="p">])</span>
<span class="k">if</span> <span class="n">p</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Bar&quot;</span><span class="p">])</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="nb">print</span><span class="p">(</span>
<span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">albums</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">persons</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&quot;__main__&quot;</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</pre></div>
</div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Export all photos to specified directory using album names as folders</span>
<span class="sd"> If file has been edited, also export the edited version,</span>
<span class="sd"> otherwise, export the original version</span>
<span class="sd"> This will result in duplicate photos if photo is in more than album &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">pathvalidate</span> <span class="kn">import</span> <span class="n">is_valid_filepath</span><span class="p">,</span> <span class="n">sanitize_filepath</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s2">&quot;export_path&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">click</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">exists</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
<span class="s2">&quot;--default-album&quot;</span><span class="p">,</span>
<span class="n">help</span><span class="o">=</span><span class="s2">&quot;Default folder for photos with no album. Defaults to &#39;unfiled&#39;&quot;</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="s2">&quot;unfiled&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
<span class="s2">&quot;--library-path&quot;</span><span class="p">,</span>
<span class="n">help</span><span class="o">=</span><span class="s2">&quot;Path to Photos library, default to last used library&quot;</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">export</span><span class="p">(</span><span class="n">export_path</span><span class="p">,</span> <span class="n">default_album</span><span class="p">,</span> <span class="n">library_path</span><span class="p">):</span>
<span class="n">export_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">expanduser</span><span class="p">(</span><span class="n">export_path</span><span class="p">)</span>
<span class="n">library_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">expanduser</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span> <span class="k">if</span> <span class="n">library_path</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">library_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">()</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
<span class="n">albums</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">albums</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">albums</span><span class="p">:</span>
<span class="n">albums</span> <span class="o">=</span> <span class="p">[</span><span class="n">default_album</span><span class="p">]</span>
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="n">albums</span><span class="p">:</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;exporting </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> in album </span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># make sure no invalid characters in destination path (could be in album name)</span>
<span class="n">album_name</span> <span class="o">=</span> <span class="n">sanitize_filepath</span><span class="p">(</span><span class="n">album</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">&quot;auto&quot;</span><span class="p">)</span>
<span class="c1"># create destination folder, if necessary, based on album name</span>
<span class="n">dest_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">export_path</span><span class="p">,</span> <span class="n">album_name</span><span class="p">)</span>
<span class="c1"># verify path is a valid path</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_valid_filepath</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">&quot;auto&quot;</span><span class="p">):</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Invalid filepath </span><span class="si">{</span><span class="n">dest_dir</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># create destination dir if needed</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">isdir</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">):</span>
<span class="n">os</span><span class="o">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
<span class="c1"># export the photo</span>
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="c1"># export edited version</span>
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">edited</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Exported </span><span class="si">{</span><span class="n">edited_name</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># export unedited version</span>
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Exported </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Skipping missing photo: </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&quot;__main__&quot;</span><span class="p">:</span>
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
</pre></div>
</div>
</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></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>
</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>
</section>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="#">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="#">Documentation overview</a><ul>
<li>Next: <a href="cli.html" title="next chapter">osxphotos command line interface (CLI)</a></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/index.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

107
docs/modules.html Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<section id="osxphotos">
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="toctree-wrapper compound">
</div>
</section>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/modules.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

6
docs/objects.inv Normal file
View File

@@ -0,0 +1,6 @@
# Sphinx inventory version 2
# Project: osxphotos
# Version:
# The remainder of this file is compressed using zlib.
xÚ…<EFBFBD>¿
Â0‡÷<ŹéPÁÕÕ© PppŽÉÙ“»Ò¤ ›¯áëù$¶MµÜ’ßýò}ä”5àƒÞjV<6A>l@Y³.³ÀþV؃bç$i°† ¬.R!,wût%r$CoÃÊ3ÚòI#*m<>b¨õªQã„¶± ¼¯ÇÓCS­R<>Á0-„cýÃVÞ“Ï â¬k½×u?6÷Ñì£bÄù#Èî¡`šz*¼`…Ôlfdú†ó¥R]eŽÂ£¬T1ÕÅ,¾8ÆyÖvßžM™Å

BIN
docs/osxphotos.pdf Normal file

Binary file not shown.

113
docs/reference.html Normal file
View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<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 package &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="prev" title="osxphotos command line interface (CLI)" href="cli.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<section id="osxphotos-package">
<h1>osxphotos package<a class="headerlink" href="#osxphotos-package" title="Permalink to this headline"></a></h1>
<section id="osxphotos-module">
<h2>osxphotos module<a class="headerlink" href="#osxphotos-module" title="Permalink to this headline"></a></h2>
</section>
</section>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1 current"><a class="current reference internal" href="#">osxphotos package</a><ul>
<li class="toctree-l2"><a class="reference internal" href="#osxphotos-module">osxphotos module</a></li>
</ul>
</li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
<li>Previous: <a href="cli.html" title="previous chapter">osxphotos command line interface (CLI)</a></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/reference.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

123
docs/search.html Normal file
View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.44.8 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/searchtools.js"></script>
<script src="_static/language_data.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="#" />
<script src="searchindex.js" defer></script>
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1 id="search-documentation">Search</h1>
<noscript>
<div class="admonition warning">
<p>
Please activate JavaScript to enable the search
functionality.
</p>
</div>
</noscript>
<p>
Searching for multiple words only shows matches that contain
all words.
</p>
<form action="" method="get">
<input type="text" name="q" aria-labelledby="search-documentation" value="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="search" />
<span id="search-progress" style="padding-left: 10px"></span>
</form>
<div id="search-results">
</div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

1
docs/searchindex.js Normal file

File diff suppressed because one or more lines are too long

367
docs/tutorial.html Normal file
View File

@@ -0,0 +1,367 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Export your photos &#8212; osxphotos 0.42.20 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
# OSXPhotos Tutorial
## Tutorial
<!-- OSXPHOTOS-TUTORIAL-HEADER:END --><p>The design philosophy for osxphotos is “make the easy things easy and make the hard things possible”. To “make the hard things possible”, osxphotos is very flexible and has many, many configuration options the <code class="docutils literal notranslate"><span class="pre">export</span></code> command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the <code class="docutils literal notranslate"><span class="pre">export</span></code> command which exports photos from the library so thats the focus of this tutorial.</p>
<div class="section" id="export-your-photos">
<h1>Export your photos<a class="headerlink" href="#export-your-photos" title="Permalink to this headline"></a></h1>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span></code></p>
<p>This command exports all your photos to the <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code> directory.</p>
<p><strong>Note</strong>: osxphotos uses the term photo to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple “Live Photo” which is an image and an associated “live preview” video file), a JPEG image with an associated RAW image, etc.</p>
</div>
<div class="section" id="export-by-date">
<h1>Export by date<a class="headerlink" href="#export-by-date" title="Permalink to this headline"></a></h1>
<p>While the previous command will export all your photos (and videossee note above), it probably doesnt do exactly what you want. In the previous example, all the photos will be exported to a single folder: <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code>. If you have a large library with thousands of images and videos, this likely isnt very useful. You can use the <code class="docutils literal notranslate"><span class="pre">--export-by-date</span></code> option to export photos to a folder structure organized by year, month, day, e.g. <code class="docutils literal notranslate"><span class="pre">2021/04/21</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--export-by-date</span></code></p>
<p>With this command, a photo that was created on 31 May 2015 would be exported to: <code class="docutils literal notranslate"><span class="pre">/path/to/export/2015/05/31</span></code></p>
</div>
<div class="section" id="specify-directory-structure">
<h1>Specify directory structure<a class="headerlink" href="#specify-directory-structure" title="Permalink to this headline"></a></h1>
<p>If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START --></span>template system<span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --></span> that allows you to specify the directory structure using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option. For example, this command exported to a directory structure that looks like: <code class="docutils literal notranslate"><span class="pre">2015/May</span></code> (4-digit year / month name):</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{created.month}&quot;</span></code></p>
<p>The string following <code class="docutils literal notranslate"><span class="pre">--directory</span></code> is an <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">template</span> <span class="pre">string</span></code>. Template strings are widely used throughout osxphotos and its worth your time to learn more about them. In a template string, the values between the curly braces, e.g. <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> are replaced with metadata from the photo being exported. In this case, <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> is the 4-digit year of the photos creation date and <code class="docutils literal notranslate"><span class="pre">{created.month}</span></code> is the full month name in the users locale (e.g. <code class="docutils literal notranslate"><span class="pre">May</span></code>, <code class="docutils literal notranslate"><span class="pre">mai</span></code>, etc.). In the osxphotos template system these are referred to as template fields. The text not included between <code class="docutils literal notranslate"><span class="pre">{}</span></code> pairs is interpreted literally, in this case <code class="docutils literal notranslate"><span class="pre">/</span></code>, is a directory separator.</p>
<p>osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> template, you could thus export photos organized by country name:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{place.name.country}&quot;</span></code></p>
<p>Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{place.name.country,No-Country}&quot;</span></code></p>
<p>The value after the , in the template string is the default value, in this case No-Country. <strong>Note</strong>: If you dont specify a default value and a template field is null, osxphotos will use “_” (underscore character) as the default.</p>
<p>Some template fields, such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code>, may expand to more than one value. For example, if a photo has keywords of “Travel” and “Vacation”, <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> would expand to “Travel”, “Vacation”. When used with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{keyword}&quot;</span></code></p>
<p>the exported files would be:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
</pre></div>
</div>
</div>
<div class="section" id="specify-exported-filename">
<h1>Specify exported filename<a class="headerlink" href="#specify-exported-filename" title="Permalink to this headline"></a></h1>
<p>By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> or <code class="docutils literal notranslate"><span class="pre">DSC05678.dng</span></code>. osxphotos allows you to specify a custom filename template using the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option in the same way as <code class="docutils literal notranslate"><span class="pre">--directory</span></code> allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">&quot;{title}&quot;</span></code></p>
<p>The above command will export photos using the title. Note that you dont need to specify the extension as part of the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:</p>
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename &quot;{title,{original_name}}&quot;
│ ││ │
│ ││ │
Use photo&#39;s title as the filename &lt;──────┘ ││ │
││ │
Value after comma will be used &lt;───────┘│ │
if title is blank │ │
│ │
The default value can be &lt;────┘ │
another template field │
Use photo&#39;s original name if no title &lt;──────┘
</pre></div>
</div>
<p>The osxphotos template system also allows for limited conditional logic of the type “If a condition is true then do one thing, otherwise, do a different thing”. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option to name files that are marked as “Favorites” in Photos differently than other files. For example, to add a “#” to the name of every photo thats a favorite:</p>
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename &quot;{original_name}{favorite?#,}&quot;
│ │ │││
│ │ │││
Use photo&#39;s original name as filename &lt;──┘ │ │││
│ │││
&#39;favorite&#39; is True if photo is a Favorite, &lt;───────┘ │││
otherwise, False │││
│││
&#39;?&#39; specifies a conditional &lt;─────────────┘││
││
Value immediately following ? will be used if &lt;──────┘│
preceding template field is True or non-blank │
Value immediately following comma will be used if &lt;──────┘
template field is False or blank (null); in this case
no value is specified so a blank string &quot;&quot; will be used
</pre></div>
</div>
<p>Like with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, using a multi-valued template field such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> may result in more than one copy of a photo being exported. For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">&quot;{keyword}-{original_name}&quot;</span></code></p>
<p>the exported files would be:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
</pre></div>
</div>
</div>
<div class="section" id="edited-photos">
<h1>Edited photos<a class="headerlink" href="#edited-photos" title="Permalink to this headline"></a></h1>
<p>If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append “_edited” to the edited image. For example, if the original image was named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code>, osxphotos will export the original as <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the edited version as <code class="docutils literal notranslate"><span class="pre">IMG_1234_edited.jpeg</span></code>. <strong>Note:</strong> Photos changes the extension of edited images to “.jpeg” even if the original was named “.JPG”. You can change the suffix appended to edited images using the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">&quot;_EDIT&quot;</span></code></p>
<p>In this example, the edited image would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234_EDIT.jpeg</span></code>. Like many options in osxphotos, the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">&quot;_{modified.year}-{modified.mm}-{modified.dd}&quot;</span></code></p>
<p>In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: <code class="docutils literal notranslate"><span class="pre">IMG_1234_2021-04-21.jpeg</span></code>.</p>
<p>You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using <code class="docutils literal notranslate"><span class="pre">--skip-edited</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-edited</span></code></p>
<p>You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the <code class="docutils literal notranslate"><span class="pre">--skip-original-if-edited</span></code> option:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-original-if-edited</span></code></p>
<p>As mentioned above, Photos renames JPEG images that have been edited with the “.jpeg” extension. Some applications use “.JPG” and others use “.jpg” or “.JPEG”. You can use the <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span></code> option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code> to use .jpg for all JPEGs.</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code></p>
</div>
<div class="section" id="specifying-the-photos-library">
<h1>Specifying the Photos library<a class="headerlink" href="#specifying-the-photos-library" title="Permalink to this headline"></a></h1>
<p>All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the “Option” key while opening Photos, you can select an alternate Photos library. If you dont specify which library to use, osxphotos will try find the last opened library. Occasionally it cant determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the <code class="docutils literal notranslate"><span class="pre">--db</span></code> option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--db</span> <span class="pre">~/Pictures/MyAlternateLibrary.photoslibrary</span></code></p>
</div>
<div class="section" id="missing-photos">
<h1>Missing photos<a class="headerlink" href="#missing-photos" title="Permalink to this headline"></a></h1>
<p>osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasnt yet synched the cloud library to the local Mac or you have Photos configured to “Optimize Mac Storage” in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.</p>
<p>If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> option. <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. Theres also an experimental <code class="docutils literal notranslate"><span class="pre">--use-photokit</span></code> option that will communicate with Photos using a different “PhotoKit” interface. This option must be used together with <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span></code></p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span> <span class="pre">--use-photokit</span></code></p>
</div>
<div class="section" id="exporting-to-external-disks">
<h1>Exporting to external disks<a class="headerlink" href="#exporting-to-external-disks" title="Permalink to this headline"></a></h1>
<p>If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the <code class="docutils literal notranslate"><span class="pre">--retry</span></code> option so that osxphotos will automatically retry the export. Use <code class="docutils literal notranslate"><span class="pre">--retry</span></code> with a number that specifies the number of times to retry the export:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--retry</span> <span class="pre">3</span></code></p>
<p>In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.</p>
</div>
<div class="section" id="exporting-metadata-with-exported-photos">
<h1>Exporting metadata with exported photos<a class="headerlink" href="#exporting-metadata-with-exported-photos" title="Permalink to this headline"></a></h1>
<p>Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free <cite>``exiftool`</cite> &lt;<a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>&gt;`_ app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> option to write metadata to exported photos:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span></code></p>
<p>This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify the metadata that is written by <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:</p>
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --exiftool --keyword-template &quot;{folder_album(&gt;)}&quot;
│ │
│ │
folder_album results in the folder(s) &lt;──┘ │
and album a photo is contained in │
The value in () is used as the path separator &lt;───────┘
for joining the folders and albums. For example,
if photo is in Folder1/Folder2/Album, (&gt;) produces
&quot;Folder1&gt;Folder2&gt;Album&quot; which some programs, such as
Lightroom Classic, treat as hierarchal keywords
</pre></div>
</div>
<p>The above command will write all the regular metadata that <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form “Folder1&gt;Folder2&gt;Album”. If you did not include the <code class="docutils literal notranslate"><span class="pre">(&gt;)</span></code> in the template string (e.g. <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>), folder_album would render in form “Folder1/Folder2/Album”.</p>
<p>A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the <code class="docutils literal notranslate"><span class="pre">{label}</span></code>. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the images metadata:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label}&quot;</span></code></p>
<p><strong>Note</strong>: When evaluating templates for <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code>, osxphotos inserts the automatic default value “_” for any template field which is null (empty or blank). This is to ensure that theres never a null directory or filename created. For metadata templates such as <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code>, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add “nolabel” as a keyword for any photo that doesnt have labels:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label,nolabel}&quot;</span></code></p>
</div>
<div class="section" id="sidecar-files">
<h1>Sidecar files<a class="headerlink" href="#sidecar-files" title="Permalink to this headline"></a></h1>
<p>Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> option. For example, to output metadata to XMP sidecars:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span></code></p>
<p>Unlike <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code>, you do not need to install exiftool to use the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> feature. Many of the same configuration options that apply to <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify metadata, for example, <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> can also be used with <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code>.</p>
<p>Sidecar files are named “photoname.ext.sidecar_ext”. For example, if the photo is named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the sidecar format is XMP, the sidecar would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG.XMP</span></code>. Some applications expect the sidecar in this case to be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.XMP</span></code>. You can use the <code class="docutils literal notranslate"><span class="pre">-sidecar-drop-ext</span></code> option to force osxphotos to name the sidecar files in this manner:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span> <span class="pre">-sidecar-drop-ext</span></code></p>
</div>
<div class="section" id="updating-a-previous-export">
<h1>Updating a previous export<a class="headerlink" href="#updating-a-previous-export" title="Permalink to this headline"></a></h1>
<p>If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option. When <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> is run, it creates a database file named <code class="docutils literal notranslate"><span class="pre">.osxphotos_export.db</span></code> in the export folder. (<strong>Note</strong> Because the filename starts with a “.”, you wont see it in Finder which treats “dot-files” like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span></code></p>
<p>will read the export database located in <code class="docutils literal notranslate"><span class="pre">/path/to/export/.osxphotos_export.db</span></code> and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option even if its never been run before. If the database isnt found, osxphotos will create it. If you run <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> without <code class="docutils literal notranslate"><span class="pre">--update</span></code> in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use <code class="docutils literal notranslate"><span class="pre">--update</span></code>.</p>
<p>If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you can use the <code class="docutils literal notranslate"><span class="pre">--only-new</span></code> with <code class="docutils literal notranslate"><span class="pre">--update</span></code> to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without <code class="docutils literal notranslate"><span class="pre">--only-new</span></code>, osxphotos would see that previously exported files are missing and re-export them.</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--only-new</span></code></p>
<p>If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you should use the <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> option. <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> instructs osxphotos to ignore the files signature (for example, size and date modified) when deciding which files should be updated with <code class="docutils literal notranslate"><span class="pre">--update</span></code>. If you edit a file in the export directory and then run <code class="docutils literal notranslate"><span class="pre">--update</span></code> without <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code>, osxphotos will see that the file is different than the one in the Photos library and re-export it.</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--ignore-signature</span></code></p>
</div>
<div class="section" id="dry-run">
<h1>Dry Run<a class="headerlink" href="#dry-run" title="Permalink to this headline"></a></h1>
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--dry-run</span></code> option to have osxphotos “dry run” or test an export without actually exporting any files. When combined with the <code class="docutils literal notranslate"><span class="pre">--verbose</span></code> option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code> templates are correct, <code class="docutils literal notranslate"><span class="pre">--dry-run</span> <span class="pre">--verbose</span></code> will print out the name of each file being exported.</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--dry-run</span> <span class="pre">--verbose</span></code></p>
</div>
<div class="section" id="creating-a-report-of-all-exported-files">
<h1>Creating a report of all exported files<a class="headerlink" href="#creating-a-report-of-all-exported-files" title="Permalink to this headline"></a></h1>
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--report</span> <span class="pre">export.csv</span></code></p>
</div>
<div class="section" id="exporting-only-certain-photos">
<h1>Exporting only certain photos<a class="headerlink" href="#exporting-only-certain-photos" title="Permalink to this headline"></a></h1>
<p>By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of “query options” that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of themread the help text (<code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">export</span></code>) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.</p>
<p>For example, to export only photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">&quot;Travel&quot;</span></code></p>
<p>Like many options in osxphotos, <code class="docutils literal notranslate"><span class="pre">--keyword</span></code> (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code> <em>or</em> keyword <code class="docutils literal notranslate"><span class="pre">Vacation</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">&quot;Travel&quot;</span> <span class="pre">--keyword</span> <span class="pre">&quot;Vacation&quot;</span></code></p>
<p>To export only photos contained in the album “Summer Vacation”:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--album</span> <span class="pre">&quot;Summer</span> <span class="pre">Vacation&quot;</span></code></p>
<p>There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone “Portrait Mode”:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--portrait</span></code></p>
<p>You can also export photos in a certain date range:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--from-date</span> <span class="pre">&quot;2020-01-01&quot;</span> <span class="pre">--to-date</span> <span class="pre">&quot;2020-02-28&quot;</span></code></p>
</div>
<div class="section" id="converting-images-to-jpeg-on-export">
<h1>Converting images to JPEG on export<a class="headerlink" href="#converting-images-to-jpeg-on-export" title="Permalink to this headline"></a></h1>
<p>Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the <code class="docutils literal notranslate"><span class="pre">--convert-to-jpeg</span></code> option. You can specify the JPEG quality (0: worst, 1.0: best) using <code class="docutils literal notranslate"><span class="pre">--jpeg-quality</span></code>. For example:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--convert-to-jpeg</span> <span class="pre">--jpeg-quality</span> <span class="pre">0.9</span></code></p>
</div>
<div class="section" id="finder-attributes">
<h1>Finder attributes<a class="headerlink" href="#finder-attributes" title="Permalink to this headline"></a></h1>
<p>In addition to using <code class="docutils literal notranslate"><span class="pre">exiftool</span></code> to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.</p>
<p>osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the <code class="docutils literal notranslate"><span class="pre">tag:tagname</span></code> syntax:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span></code></p>
<p><code class="docutils literal notranslate"><span class="pre">--finder-tag-keywords</span></code> also works with <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> as described above in the section on <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label}&quot;</span></code></p>
<p>The <code class="docutils literal notranslate"><span class="pre">--xattr-template</span></code> option allows you to set a variety of other extended attributes. It is used in the format <code class="docutils literal notranslate"><span class="pre">--xattr-template</span> <span class="pre">ATTRIBUTE</span> <span class="pre">TEMPLATE</span></code> where ATTRIBUTE is one of authors,comment, copyright, description, findercomment, headline, keywords.</p>
<p>For example, to set Finder comment to the photos title and description:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">&quot;{title}{newline}{descr}&quot;</span></code></p>
<p>In the template string above, <code class="docutils literal notranslate"><span class="pre">{newline}</span></code> instructs osxphotos to insert a new line character (“n”) between the title and description. In this example, if <code class="docutils literal notranslate"><span class="pre">{title}</span></code> or <code class="docutils literal notranslate"><span class="pre">{descr}</span></code> is empty, youll get “titlen” or “ndescription” which may not be desired so you can use more advanced features of the template system to handle these cases:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">&quot;{title}{title?{descr?{newline},},}{descr}&quot;</span></code></p>
<p>Explanation of the template string:</p>
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>{title}{title?{descr?{newline},},}{descr}
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──&gt; insert title │ │ │ │ │
│ │ │ │ │ │
└───&gt; is there a title?
│ │ │ │ │
└───&gt; if so, is there a description?
│ │ │ │
└───&gt; if so, insert new line
│ │ │
└───&gt; if descr is blank, insert nothing
│ │
└───&gt; if title is blank, insert nothing
└───&gt; finally, insert description
</pre></div>
</div>
<p>In this example, <code class="docutils literal notranslate"><span class="pre">title?</span></code> demonstrates use of the boolean (True/False) feature of the template system. <code class="docutils literal notranslate"><span class="pre">title?</span></code> is read as “Is the title True (or not blank/empty)? If so, then the value immediately following the <code class="docutils literal notranslate"><span class="pre">?</span></code> is used in place of <code class="docutils literal notranslate"><span class="pre">title</span></code>. If <code class="docutils literal notranslate"><span class="pre">title</span></code> is blank, then the value immediately following the comma is used instead. The format for boolean fields is <code class="docutils literal notranslate"><span class="pre">field?value</span> <span class="pre">if</span> <span class="pre">true,value</span> <span class="pre">if</span> <span class="pre">false</span></code>. Either <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">true</span></code> or <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">false</span></code> may be blank, in which case a blank string (“”) is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex <code class="docutils literal notranslate"><span class="pre">if-then-else</span></code> statements.</p>
<p>The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.</p>
<p>See Extended Attributes section in the help for <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> for additional information about this feature.</p>
</div>
<div class="section" id="saving-and-loading-options">
<h1>Saving and loading options<a class="headerlink" href="#saving-and-loading-options" title="Permalink to this headline"></a></h1>
<p>If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (<code class="docutils literal notranslate"><span class="pre">--save-config</span> <span class="pre">FILE</span></code>) and then load them (<code class="docutils literal notranslate"><span class="pre">--load-config</span> <span class="pre">FILE</span></code>) instead of repeating each option on the command line.</p>
<p>To save the configuration:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">&lt;all</span> <span class="pre">your</span> <span class="pre">options</span> <span class="pre">here&gt;</span> <span class="pre">--update</span> <span class="pre">--save-config</span> <span class="pre">osxphotos.toml</span></code></p>
<p>Then the next to you run osxphotos, you can simply do this:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--load-config</span> <span class="pre">osxphotos.toml</span></code></p>
<p>The configuration file is a plain text file in <a class="reference external" href="https://toml.io/en/">TOML</a> format so the <code class="docutils literal notranslate"><span class="pre">.toml</span></code> extension is standard but you can name the file anything you like.</p>
</div>
<div class="section" id="an-example-from-an-actual-osxphotos-user">
<h1>An example from an actual osxphotos user<a class="headerlink" href="#an-example-from-an-actual-osxphotos-user" title="Permalink to this headline"></a></h1>
<p>Heres a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>I usually import my iPhones photo roll on a more or less regular basis, and it
includes photos and videos. As a result, the size ot my Photos library may rise
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
quite good keyword management system.
After a while, I want to take most of the videos out of the library and move them
to a separate &quot;videos&quot; folder on a different folder / volume. As I might want to
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
its internal library tagging system, I will use osxphotos to do just this.
Picking the videos can be left to Photos, using a smart folder for instance. Then
just add a keyword to all videos to be processed. Here I chose &quot;Quik&quot; as I wanted
to spot all videos created on my iPhone using the Quik application (now part of
GoPro).
I want to retrieve my keywords only and make sure they populate the Finder tags, as
well as export all the persons identified in the videos by Photos. I also want to
merge any keywords or persons already in the video metadata with the exported
metadata.
Keeping Photos edited titles and descriptions and putting both in the Finder
comments field in a readable manner is also enabled.
And I want to keep the files creation date (using `--touch-file`).
Finally, use `--strip` to remove any leading or trailing whitespace from processed
template fields.
</pre></div>
</div>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/folder</span> <span class="pre">for</span> <span class="pre">exported</span> <span class="pre">videos/</span> <span class="pre">--keyword</span> <span class="pre">Quik</span> <span class="pre">--only-movies</span> <span class="pre">--db</span> <span class="pre">/path</span> <span class="pre">to</span> <span class="pre">my.photoslibrary</span> <span class="pre">--touch-file</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--person-keyword</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">&quot;{title}{title?{descr?{newline},},}{descr}&quot;</span> <span class="pre">--exiftool-merge-keywords</span> <span class="pre">--exiftool-merge-persons</span> <span class="pre">--exiftool</span> <span class="pre">--strip</span></code></p>
</div>
<div class="section" id="conclusion">
<h1>Conclusion<a class="headerlink" href="#conclusion" title="Permalink to this headline"></a></h1>
<p>osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.</p>
</div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<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></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<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="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/tutorial.md.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

28
docsrc/Makefile Normal file
View File

@@ -0,0 +1,28 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
github:
@make html
@cp -a _build/html/. ../docs
pdf:
@make latexpdf
@cp -a _build/latex/osxphotos.pdf ../docs
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

13
docsrc/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Building the documentation
I'm still trying to learn sphinx and come up with a workflow for building docs. Right now it's pretty kludgy.
- `pip install sphinx`
- `pip install sphinx-rtd-theme`
- `pip install m2r2`
- Download and install [MacTeX](https://tug.org/mactex/)
- Add `/Library/TeX/texbin` to your `$PATH`
- `cd docs`
- `make html`
- `make latexpdf`
- `cp _build/latex/osxphotos.pdf .`

35
docsrc/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

6
docsrc/source/cli.rst Normal file
View File

@@ -0,0 +1,6 @@
osxphotos command line interface (CLI)
======================================
.. click:: osxphotos.cli:cli
:prog: osxphotos
:nested: full

69
docsrc/source/conf.py Normal file
View File

@@ -0,0 +1,69 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import pathlib
import sys
import sphinx_rtd_theme
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
project = "osxphotos"
copyright = "2021, Rhet Turnbull"
author = "Rhet Turnbull"
# holds config info read from disk
about = {}
this_directory = pathlib.Path(__file__).parent
version_file = this_directory.parent.parent / "osxphotos" / "_version.py"
# get version info from _version
with open(
version_file, mode="r", encoding="utf-8"
) as f:
exec(f.read(), about)
# The full version, including alpha/beta/rc tags
release = about["__version__"]
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx_click", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "m2r2"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

23
docsrc/source/index.rst Normal file
View File

@@ -0,0 +1,23 @@
.. osxphotos documentation master file, created by
sphinx-quickstart on Sat Jan 23 13:27:27 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to osxphotos's documentation!
=====================================
.. include:: ../../README.rst
.. toctree::
:maxdepth: 4
cli
reference
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -0,0 +1,5 @@
osxphotos
===========
.. toctree::
:maxdepth: 4

View File

@@ -0,0 +1,13 @@
osxphotos package
===================
osxphotos module
------------------------------
.. autoclass:: osxphotos.PhotosDB
:members:
:undoc-members:
.. autoclass:: osxphotos.PhotoInfo
:members:
:undoc-members:

View File

@@ -0,0 +1,112 @@
""" Example function for use with osxphotos export --post-function option showing how to record album sort order """
import os
import pathlib
from typing import Optional
from osxphotos import ExportResults, PhotoInfo
from osxphotos.albuminfo import AlbumInfo
from osxphotos.path_utils import sanitize_dirname
from osxphotos.phototemplate import RenderOptions
def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
"""Call this with {function} template to get album sequence (sort order) when exporting with {folder_album} template
For example, calling this template function like the following prepends sequence#_ to each exported file if the file is in an album:
osxphotos export /path/to/export -V --directory "{folder_album}" --filename "{album?{function:examples/album_sort_order.py::album_sequence}_,}{original_name}"
The sequence will start at 0. To change the sequence to start at a different offset (e.g. 1), set the environment variable OSXPHOTOS_ALBUM_SEQUENCE_START=1 (or whatever offset you want)
"""
dest_path = options.dest_path
if not dest_path:
return ""
album_info = None
for album in photo.album_info:
# following code is how {folder_album} builds the folder path
folder = "/".join(sanitize_dirname(f) for f in album.folder_names)
folder += "/" + sanitize_dirname(album.title)
if dest_path.endswith(folder):
album_info = album
break
else:
# didn't find the album, so skip this file
return ""
start_index = int(os.getenv("OSXPHOTOS_ALBUM_SEQUENCE_START", 0))
return str(album_info.photo_index(photo) + start_index)
def album_sort_order(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
):
"""Call this with osxphotos export /path/to/export --post-function album_sort_order.py::album_sort_order
This will get called immediately after the photo has been exported
Args:
photo: PhotoInfo instance for the photo that's just been exported
results: ExportResults instance with information about the files associated with the exported photo
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
Notes:
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
Will not be called if --dry-run flag is enabled
Will be called immediately after export and before any --post-command commands are executed
"""
# ExportResults has the following properties
# fields with filenames contain the full path to the file
# exported: list of all files exported
# new: list of all new files exported (--update)
# updated: list of all files updated (--update)
# skipped: list of all files skipped (--update)
# exif_updated: list of all files that were updated with --exiftool
# touched: list of all files that had date updated with --touch-file
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
# sidecar_json_written: list of all JSON sidecar files written
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
# sidecar_exiftool_written: list of all exiftool sidecar files written
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
# sidecar_xmp_written: list of all XMP sidecar files written
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
# missing: list of all missing files
# error: list tuples of (filename, error) for any errors generated during export
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
# xattr_written: list of files that had extended attributes written
# xattr_skipped: list of files that where extended attributes were skipped (--update)
# deleted_files: list of deleted files
# deleted_directories: list of deleted directories
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
for filepath in results.exported:
# do your processing here
filepath = pathlib.Path(filepath)
album_dir = filepath.parent.name
if album_dir not in photo.albums:
return
# get the first album that matches this name of which the photo is a member
album_info = None
for album in photo.album_info:
if album.title == album_dir:
album_info = album
break
else:
# didn't find the album, so skip this file
return
try:
sort_order = album_info.photo_index(photo)
except ValueError:
# photo not in album, so skip this file
return
verbose(f"Sort order for {filepath} in album {album_dir} is {sort_order}")
with open(str(filepath) + "_sort_order.txt", "w") as f:
f.write(str(sort_order))

173
examples/export_template.py Normal file
View File

@@ -0,0 +1,173 @@
""" Example showing how to use a custom function for osxphotos {function} template
to export photos in a folder structure similar to Photos' own structure
Use: osxphotos export /path/to/export --directory "{function:/path/to/export_template.py::photos_folders}"
This will likely export multiple copies of each photo. If using APFS file system, this should be
a non-issue as osxphotos will use copy-on-write so each exported photo doesn't take up additional space
unless you edit the photo.
Thank-you @mkirkland4874 for the inspiration for this example!
This will produce output similar to this:
Library
- Photos
-- {created.year}
---- {created.mm}
------ {created.dd}
- Favorites
- Hidden
- Recently Deleted
- People
- Places
- Imports
Media Types
- Videos
- Selfies
- Portrait
- Panoramas
- Time-lapse
- Slow-mo
- Bursts
- Screenshots
My Albums
-- Album 1
-- Album 2
-- Folder 1
---- Album 3
Shared Albums
-- Shared Album 1
-- Shared Album 2
"""
from typing import List, Union
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.datetime_formatter import DateTimeFormatter
from osxphotos.path_utils import sanitize_dirname
from osxphotos.phototemplate import RenderOptions
def place_folder(photo: osxphotos.PhotoInfo) -> str:
"""Return places as folder in format Country/State/City/etc."""
if not photo.place:
return ""
places = []
if photo.place.names.country:
places.append(photo.place.names.country[0])
if photo.place.names.state_province:
places.append(photo.place.names.state_province[0])
if photo.place.names.sub_administrative_area:
places.append(photo.place.names.sub_administrative_area[0])
if photo.place.names.additional_city_info:
places.append(photo.place.names.additional_city_info[0])
if photo.place.names.area_of_interest:
places.append(photo.place.names.area_of_interest[0])
if places:
return "Library/Places/" + "/".join(sanitize_dirname(place) for place in places)
else:
return ""
def photos_folders(photo: osxphotos.PhotoInfo, options: osxphotos.phototemplate.RenderOptions, **kwargs) -> Union[List, str]:
"""template function for use with --directory to export photos in a folder structure similar to Photos
Args:
photo: osxphotos.PhotoInfo object
options: RenderOptions instance
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns: list of directories for each photo
"""
rendered_date, _ = photo.render_template("{created.year}/{created.mm}/{created.dd}")
date_path = rendered_date[0]
def add_date_path(path):
"""add date path (year/mm/dd)"""
return f"{path}/{date_path}"
# Library
directories = []
if not photo.hidden and not photo.intrash and not photo.shared:
# set directories to [Library/Photos/year/mm/dd]
# render_template returns a tuple of [rendered value(s)], [unmatched]
# here, we can ignore the unmatched value, assigned to _, as we know template will match
directories, _ = photo.render_template(
"Library/Photos/{created.year}/{created.mm}/{created.dd}"
)
if photo.favorite:
directories.append(add_date_path("Library/Favorites"))
if photo.hidden:
directories.append(add_date_path("Library/Hidden"))
if photo.intrash:
directories.append(add_date_path("Library/Recently Deleted"))
directories.extend(
[
add_date_path(f"Library/People/{person}")
for person in photo.persons
if person != _UNKNOWN_PERSON
]
)
if photo.place:
directories.append(add_date_path(place_folder(photo)))
if photo.import_info:
dt = DateTimeFormatter(photo.import_info.creation_date)
directories.append(f"Library/Imports/{dt.year}/{dt.mm}/{dt.dd}")
# Media Types
if photo.ismovie:
directories.append(add_date_path("Media Types/Videos"))
if photo.selfie:
directories.append(add_date_path("Media Types/Selfies"))
if photo.live_photo:
directories.append(add_date_path("Media Types/Live Photos"))
if photo.portrait:
directories.append(add_date_path("Media Types/Portrait"))
if photo.panorama:
directories.append(add_date_path("Media Types/Panoramas"))
if photo.time_lapse:
directories.append(add_date_path("Media Types/Time-lapse"))
if photo.slow_mo:
directories.append(add_date_path("Media Types/Slo-mo"))
if photo.burst:
directories.append(add_date_path("Media Types/Bursts"))
if photo.screenshot:
directories.append(add_date_path("Media Types/Screenshots"))
# Albums
# render the folders and albums in folder/subfolder/album format
# the __NO_ALBUM__ is used as a sentinel to strip out photos not in an album
# use RenderOptions.dirname to force the rendered folder_album value to be sanitized as a valid path
# use RenderOptions.none_str to specify custom value for any photo that doesn't belong to an album so
# those can be filtered out; if not specified, none_str is "_"
folder_albums, _ = photo.render_template(
"{folder_album}", RenderOptions(dirname=True, none_str="__NO_ALBUM__")
)
root_directory = "Shared Albums/" if photo.shared else "My Albums/"
directories.extend(
[
root_directory + folder_album
for folder_album in folder_albums
if folder_album != "__NO_ALBUM__"
]
)
return directories

View File

@@ -15,7 +15,7 @@ import time
import click
import osxphotos
from osxphotos.__main__ import get_photos_db, _list_libraries
from osxphotos.cli import get_photos_db, _list_libraries
def show(photo):

54
examples/post_function.py Normal file
View File

@@ -0,0 +1,54 @@
""" Example function for use with osxphotos export --post-function option """
from osxphotos import PhotoInfo, ExportResults
def post_function(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
):
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
This will get called immediately after the photo has been exported
Args:
photo: PhotoInfo instance for the photo that's just been exported
results: ExportResults instance with information about the files associated with the exported photo
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
Notes:
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
Will not be called if --dry-run flag is enabled
Will be called immediately after export and before any --post-command commands are executed
"""
# ExportResults has the following properties
# fields with filenames contain the full path to the file
# exported: list of all files exported
# new: list of all new files exported (--update)
# updated: list of all files updated (--update)
# skipped: list of all files skipped (--update)
# exif_updated: list of all files that were updated with --exiftool
# touched: list of all files that had date updated with --touch-file
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
# sidecar_json_written: list of all JSON sidecar files written
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
# sidecar_exiftool_written: list of all exiftool sidecar files written
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
# sidecar_xmp_written: list of all XMP sidecar files written
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
# missing: list of all missing files
# error: list tuples of (filename, error) for any errors generated during export
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
# xattr_written: list of files that had extended attributes written
# xattr_skipped: list of files that where extended attributes were skipped (--update)
# deleted_files: list of deleted files
# deleted_directories: list of deleted directories
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
for filename in results.exported:
# do your processing here
verbose(f"post_function: {photo.original_filename} exported as {filename}")

View File

@@ -0,0 +1,31 @@
""" example function for osxphotos --query-function """
from typing import List
from osxphotos import PhotoInfo
# call this with --query-function examples/query_function.py::best_selfies
def best_selfies(photos: List[PhotoInfo]) -> List[PhotoInfo]:
"""your query function should take a list of PhotoInfo objects and return a list of PhotoInfo objects (or empty list)"""
# this example finds your best selfie for every year
# get list of selfies sorted by date
photos = sorted([p for p in photos if p.selfie], key=lambda p: p.date)
if not photos:
return []
start_year = photos[0].date.year
stop_year = photos[-1].date.year
best_selfies = []
for year in range(start_year, stop_year + 1):
# find best selfie each year as determined by overall aesthetic score
selfies = sorted(
[p for p in photos if p.date.year == year],
key=lambda p: p.score.overall,
reverse=True,
)
if selfies:
best_selfies.append(selfies[0])
return best_selfies

View File

@@ -0,0 +1,17 @@
""" Example of using a custom python function as an osxphotos template filter
Use in formath:
"{template_field|template_filter.py::myfilter}"
Your filter function will receive a list of strings even if the template renders to a single value.
You should expect a list and return a list and be able to handle multi-value templates like {keyword}
as well as single-value templates like {original_name}
"""
from typing import List
def myfilter(values: List[str]) -> List[str]:
""" Custom filter to append "foo-" to template value """
values = ["foo-" + val for val in values]
return values

View File

@@ -0,0 +1,30 @@
""" Example showing how to use a custom function for osxphotos {function} template
Use: osxphotos export /path/to/export --filename "{function:/path/to/template_function.py::example}"
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
"""
import pathlib
from typing import List, Union
import osxphotos
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
""" example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
Args:
photo: osxphotos.PhotoInfo object
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:
str or list of str of values that should be substituted for the {function} template
"""
filename = pathlib.Path(photo.original_filename).stem
if photo.hasadjustments:
filename += "#"
if photo.favorite:
filename += "!"
return filename

View File

@@ -8,41 +8,50 @@ import importlib
pathex = os.getcwd()
# include necessary data files
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
package_imports = [['photoscript', ['photoscript.applescript']]]
datas = [
("osxphotos/templates/xmp_sidecar.mako", "osxphotos/templates"),
("osxphotos/templates/xmp_sidecar_beta.mako", "osxphotos/templates"),
("osxphotos/phototemplate.tx", "osxphotos"),
("osxphotos/phototemplate.md", "osxphotos"),
("osxphotos/tutorial.md", "osxphotos"),
]
package_imports = [["photoscript", ["photoscript.applescript"]]]
for package, files in package_imports:
proot = os.path.dirname(importlib.import_module(package).__file__)
datas.extend((os.path.join(proot, f), package) for f in files)
block_cipher = None
a = Analysis(['cli.py'],
pathex=[pathex],
binaries=[],
datas=datas,
hiddenimports=['pkg_resources.py2_warn'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
a = Analysis(
["cli.py"],
pathex=[pathex],
binaries=[],
datas=datas,
hiddenimports=["pkg_resources.py2_warn"],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='osxphotos',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True )
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name="osxphotos",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
)

View File

@@ -1,10 +1,13 @@
from ._constants import AlbumSortOrder
from ._version import __version__
from .exiftool import ExifTool
from .photoexporter import ExportResults, PhotoExporter
from .photoinfo import PhotoInfo
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototemplate import PhotoTemplate
from .queryoptions import QueryOptions
from .utils import _debug, _get_logger, _set_debug
# TODO: Add test for imageTimeZoneOffsetSeconds = None
# TODO: Add test for __str__ and to_json
# TODO: Add special albums and magic albums

File diff suppressed because it is too large Load Diff

View File

@@ -1,162 +0,0 @@
""" applescript -- Easy-to-use Python wrapper for NSAppleScript """
import sys
from Foundation import NSAppleScript, NSAppleEventDescriptor, NSURL, \
NSAppleScriptErrorMessage, NSAppleScriptErrorBriefMessage, \
NSAppleScriptErrorNumber, NSAppleScriptErrorAppName, NSAppleScriptErrorRange
from .aecodecs import Codecs, fourcharcode, AEType, AEEnum
from . import kae
__all__ = ['AppleScript', 'ScriptError', 'AEType', 'AEEnum', 'kMissingValue', 'kae']
######################################################################
class AppleScript:
""" Represents a compiled AppleScript. The script object is persistent; its handlers may be called multiple times and its top-level properties will retain current state until the script object's disposal.
"""
_codecs = Codecs()
def __init__(self, source=None, path=None):
"""
source : str | None -- AppleScript source code
path : str | None -- full path to .scpt/.applescript file
Notes:
- Either the path or the source argument must be provided.
- If the script cannot be read/compiled, a ScriptError is raised.
"""
if path:
url = NSURL.fileURLWithPath_(path)
self._script, errorinfo = NSAppleScript.alloc().initWithContentsOfURL_error_(url, None)
if errorinfo:
raise ScriptError(errorinfo)
elif source:
self._script = NSAppleScript.alloc().initWithSource_(source)
else:
raise ValueError("Missing source or path argument.")
if not self._script.isCompiled():
errorinfo = self._script.compileAndReturnError_(None)[1]
if errorinfo:
raise ScriptError(errorinfo)
def __repr__(self):
s = self.source
return 'AppleScript({})'.format(repr(s) if len(s) < 100 else '{}...{}'.format(repr(s)[:80], repr(s)[-17:]))
##
def _newevent(self, suite, code, args):
evt = NSAppleEventDescriptor.appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID_(
fourcharcode(suite), fourcharcode(code), NSAppleEventDescriptor.nullDescriptor(), 0, 0)
evt.setDescriptor_forKeyword_(self._codecs.pack(args), fourcharcode(kae.keyDirectObject))
return evt
def _unpackresult(self, result, errorinfo):
if not result:
raise ScriptError(errorinfo)
return self._codecs.unpack(result)
##
source = property(lambda self: str(self._script.source()), doc="str -- the script's source code")
def run(self, *args):
""" Run the script, optionally passing arguments to its run handler.
args : anything -- arguments to pass to script, if any; see also supported type mappings documentation
Result : anything | None -- the script's return value, if any
Notes:
- The run handler must be explicitly declared in order to pass arguments.
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
- If execution fails, a ScriptError is raised.
"""
if args:
evt = self._newevent(kae.kCoreEventClass, kae.kAEOpenApplication, args)
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
else:
return self._unpackresult(*self._script.executeAndReturnError_(None))
def call(self, name, *args):
""" Call the specified user-defined handler.
name : str -- the handler's name (case-sensitive)
args : anything -- arguments to pass to script, if any; see documentation for supported types
Result : anything | None -- the script's return value, if any
Notes:
- The handler's name must be a user-defined identifier, not an AppleScript keyword; e.g. 'myCount' is acceptable; 'count' is not.
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
- If execution fails, a ScriptError is raised.
"""
evt = self._newevent(kae.kASAppleScriptSuite, kae.kASPrepositionalSubroutine, args)
evt.setDescriptor_forKeyword_(NSAppleEventDescriptor.descriptorWithString_(name),
fourcharcode(kae.keyASSubroutineName))
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
##
class ScriptError(Exception):
""" Indicates an AppleScript compilation/execution error. """
def __init__(self, errorinfo):
self._errorinfo = dict(errorinfo)
def __repr__(self):
return 'ScriptError({})'.format(self._errorinfo)
@property
def message(self):
""" str -- the error message """
msg = self._errorinfo.get(NSAppleScriptErrorMessage)
if not msg:
msg = self._errorinfo.get(NSAppleScriptErrorBriefMessage, 'Script Error')
return msg
number = property(lambda self: self._errorinfo.get(NSAppleScriptErrorNumber),
doc="int | None -- the error number, if given")
appname = property(lambda self: self._errorinfo.get(NSAppleScriptErrorAppName),
doc="str | None -- the name of the application that reported the error, where relevant")
@property
def range(self):
""" (int, int) -- the start and end points (1-indexed) within the source code where the error occurred """
range = self._errorinfo.get(NSAppleScriptErrorRange)
if range:
start = range.rangeValue().location
end = start + range.rangeValue().length
return (start, end)
else:
return None
def __str__(self):
msg = self.message
for s, v in [(' ({})', self.number), (' app={!r}', self.appname), (' range={0[0]}-{0[1]}', self.range)]:
if v is not None:
msg += s.format(v)
return msg.encode('ascii', 'replace') if sys.version_info.major < 3 else msg # 2.7 compatibility
##
kMissingValue = AEType(kae.cMissingValue) # convenience constant

View File

@@ -1,269 +0,0 @@
""" aecodecs -- Convert from common Python types to Apple Event Manager types and vice-versa. """
import datetime, struct, sys
from Foundation import NSAppleEventDescriptor, NSURL
from . import kae
__all__ = ['Codecs', 'AEType', 'AEEnum']
######################################################################
def fourcharcode(code):
""" Convert four-char code for use in NSAppleEventDescriptor methods.
code : bytes -- four-char code, e.g. b'utxt'
Result : int -- OSType, e.g. 1970567284
"""
return struct.unpack('>I', code)[0]
#######
class Codecs:
""" Implements mappings for common Python types with direct AppleScript equivalents. Used by AppleScript class. """
kMacEpoch = datetime.datetime(1904, 1, 1)
kUSRF = fourcharcode(kae.keyASUserRecordFields)
def __init__(self):
# Clients may add/remove/replace encoder and decoder items:
self.encoders = {
NSAppleEventDescriptor.class__(): self.packdesc,
type(None): self.packnone,
bool: self.packbool,
int: self.packint,
float: self.packfloat,
bytes: self.packbytes,
str: self.packstr,
list: self.packlist,
tuple: self.packlist,
dict: self.packdict,
datetime.datetime: self.packdatetime,
AEType: self.packtype,
AEEnum: self.packenum,
}
if sys.version_info.major < 3: # 2.7 compatibility
self.encoders[unicode] = self.packstr
self.decoders = {fourcharcode(k): v for k, v in {
kae.typeNull: self.unpacknull,
kae.typeBoolean: self.unpackboolean,
kae.typeFalse: self.unpackboolean,
kae.typeTrue: self.unpackboolean,
kae.typeSInt32: self.unpacksint32,
kae.typeIEEE64BitFloatingPoint: self.unpackfloat64,
kae.typeUTF8Text: self.unpackunicodetext,
kae.typeUTF16ExternalRepresentation: self.unpackunicodetext,
kae.typeUnicodeText: self.unpackunicodetext,
kae.typeLongDateTime: self.unpacklongdatetime,
kae.typeAEList: self.unpackaelist,
kae.typeAERecord: self.unpackaerecord,
kae.typeAlias: self.unpackfile,
kae.typeFSS: self.unpackfile,
kae.typeFSRef: self.unpackfile,
kae.typeFileURL: self.unpackfile,
kae.typeType: self.unpacktype,
kae.typeEnumeration: self.unpackenumeration,
}.items()}
def pack(self, data):
"""Pack Python data.
data : anything -- a Python value
Result : NSAppleEventDescriptor -- an AE descriptor, or error if no encoder exists for this type of data
"""
try:
return self.encoders[data.__class__](data) # quick lookup by type/class
except (KeyError, AttributeError) as e:
for type, encoder in self.encoders.items(): # slower but more thorough lookup that can handle subtypes/subclasses
if isinstance(data, type):
return encoder(data)
raise TypeError("Can't pack data into an AEDesc (unsupported type): {!r}".format(data))
def unpack(self, desc):
"""Unpack an Apple event descriptor.
desc : NSAppleEventDescriptor
Result : anything -- a Python value, or the original NSAppleEventDescriptor if no decoder is found
"""
decoder = self.decoders.get(desc.descriptorType())
# unpack known type
if decoder:
return decoder(desc)
# if it's a record-like desc, unpack as dict with an extra AEType(b'pcls') key containing the desc type
rec = desc.coerceToDescriptorType_(fourcharcode(kae.typeAERecord))
if rec:
rec = self.unpackaerecord(rec)
rec[AEType(kae.pClass)] = AEType(struct.pack('>I', desc.descriptorType()))
return rec
# return as-is
return desc
##
def _packbytes(self, desctype, data):
return NSAppleEventDescriptor.descriptorWithDescriptorType_bytes_length_(
fourcharcode(desctype), data, len(data))
def packdesc(self, val):
return val
def packnone(self, val):
return NSAppleEventDescriptor.nullDescriptor()
def packbool(self, val):
return NSAppleEventDescriptor.descriptorWithBoolean_(int(val))
def packint(self, val):
if (-2**31) <= val < (2**31):
return NSAppleEventDescriptor.descriptorWithInt32_(val)
else:
return self.pack(float(val))
def packfloat(self, val):
return self._packbytes(kae.typeFloat, struct.pack('d', val))
def packbytes(self, val):
return self._packbytes(kae.typeData, val)
def packstr(self, val):
return NSAppleEventDescriptor.descriptorWithString_(val)
def packdatetime(self, val):
delta = val - self.kMacEpoch
sec = delta.days * 3600 * 24 + delta.seconds
return self._packbytes(kae.typeLongDateTime, struct.pack('q', sec))
def packlist(self, val):
lst = NSAppleEventDescriptor.listDescriptor()
for item in val:
lst.insertDescriptor_atIndex_(self.pack(item), 0)
return lst
def packdict(self, val):
record = NSAppleEventDescriptor.recordDescriptor()
usrf = desctype = None
for key, value in val.items():
if isinstance(key, AEType):
if key.code == kae.pClass and isinstance(value, AEType): # AS packs records that contain a 'class' property by coercing the packed record to the descriptor type specified by the property's value (assuming it's an AEType)
desctype = value
else:
record.setDescriptor_forKeyword_(self.pack(value), fourcharcode(key.code))
else:
if not usrf:
usrf = NSAppleEventDescriptor.listDescriptor()
usrf.insertDescriptor_atIndex_(self.pack(key), 0)
usrf.insertDescriptor_atIndex_(self.pack(value), 0)
if usrf:
record.setDescriptor_forKeyword_(usrf, self.kUSRF)
if desctype:
newrecord = record.coerceToDescriptorType_(fourcharcode(desctype.code))
if newrecord:
record = newrecord
else: # coercion failed for some reason, so pack as normal key-value pair
record.setDescriptor_forKeyword_(self.pack(desctype), fourcharcode(key.code))
return record
def packtype(self, val):
return NSAppleEventDescriptor.descriptorWithTypeCode_(fourcharcode(val.code))
def packenum(self, val):
return NSAppleEventDescriptor.descriptorWithEnumCode_(fourcharcode(val.code))
#######
def unpacknull(self, desc):
return None
def unpackboolean(self, desc):
return desc.booleanValue()
def unpacksint32(self, desc):
return desc.int32Value()
def unpackfloat64(self, desc):
return struct.unpack('d', bytes(desc.data()))[0]
def unpackunicodetext(self, desc):
return desc.stringValue()
def unpacklongdatetime(self, desc):
return self.kMacEpoch + datetime.timedelta(seconds=struct.unpack('q', bytes(desc.data()))[0])
def unpackaelist(self, desc):
return [self.unpack(desc.descriptorAtIndex_(i + 1)) for i in range(desc.numberOfItems())]
def unpackaerecord(self, desc):
dct = {}
for i in range(desc.numberOfItems()):
key = desc.keywordForDescriptorAtIndex_(i + 1)
value = desc.descriptorForKeyword_(key)
if key == self.kUSRF:
lst = self.unpackaelist(value)
for i in range(0, len(lst), 2):
dct[lst[i]] = lst[i+1]
else:
dct[AEType(struct.pack('>I', key))] = self.unpack(value)
return dct
def unpacktype(self, desc):
return AEType(struct.pack('>I', desc.typeCodeValue()))
def unpackenumeration(self, desc):
return AEEnum(struct.pack('>I', desc.enumCodeValue()))
def unpackfile(self, desc):
url = bytes(desc.coerceToDescriptorType_(fourcharcode(kae.typeFileURL)).data()).decode('utf8')
return NSURL.URLWithString_(url).path()
#######
class AETypeBase:
""" Base class for AEType and AEEnum.
Notes:
- Hashable and comparable, so may be used as keys in dictionaries that map to AE records.
"""
def __init__(self, code):
"""
code : bytes -- four-char code, e.g. b'utxt'
"""
if not isinstance(code, bytes):
raise TypeError('invalid code (not a bytes object): {!r}'.format(code))
elif len(code) != 4:
raise ValueError('invalid code (not four bytes long): {!r}'.format(code))
self._code = code
code = property(lambda self:self._code, doc="bytes -- four-char code, e.g. b'utxt'")
def __hash__(self):
return hash(self._code)
def __eq__(self, val):
return val.__class__ == self.__class__ and val.code == self._code
def __ne__(self, val):
return not self == val
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self._code)
##
class AEType(AETypeBase):
"""An AE type. Maps to an AppleScript type class, e.g. AEType(b'utxt') <=> 'unicode text'."""
class AEEnum(AETypeBase):
"""An AE enumeration. Maps to an AppleScript constant, e.g. AEEnum(b'yes ') <=> 'yes'."""

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ Constants used by osxphotos
import os.path
from datetime import datetime
from enum import Enum
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
@@ -19,8 +20,8 @@ UNICODE_FORMAT = "NFC"
# Photos 3.0 (10.13.6) == 3301
# Photos 4.0 (10.14.5) == 4016
# Photos 4.0 (10.14.6) == 4025
# Photos 5.0 (10.15.0) == 6000
_TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
# Photos 5.0 (10.15.0) == 6000 or 5001
_TESTED_DB_VERSIONS = ["6000", "5001", "4025", "4016", "3301", "2622"]
# database model versions (applies to Photos 5, Photos 6)
# these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST
@@ -29,16 +30,19 @@ _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"
# versions 5.0 and later have a different database structure
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.6
_PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common on Catalina and up but there are some version 5001 database in the wild
# Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134
# some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = {
@@ -49,6 +53,10 @@ _DB_TABLE_NAMES = {
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
"DEPTH_STATE": "ZGENERICASSET.ZDEPTHSTATES",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
},
6: {
"ASSET": "ZASSET",
@@ -57,6 +65,22 @@ _DB_TABLE_NAMES = {
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
},
7: {
"ASSET": "ZASSET",
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_38KEYWORDS",
"ALBUM_JOIN": "Z_27ASSETS.Z_3ASSETS",
"ALBUM_SORT_ORDER": "Z_27ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
"ASSET_ALBUM_JOIN": "Z_27ASSETS.Z_27ALBUMS",
"ASSET_ALBUM_TABLE": "Z_27ASSETS",
"HDR_TYPE": "ZHDRTYPE",
},
}
@@ -69,6 +93,11 @@ _TESTED_OS_VERSIONS = [
("10", "16"),
("11", "0"),
("11", "1"),
("11", "2"),
("11", "3"),
("11", "4"),
("11", "5"),
("11", "6"),
]
# Photos 5 has persons who are empty string if unidentified face
@@ -94,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
@@ -186,6 +223,9 @@ DEFAULT_EDITED_SUFFIX = "_edited"
# Default suffix to add to original images
DEFAULT_ORIGINAL_SUFFIX = ""
# Default suffix to add to preview images
DEFAULT_PREVIEW_SUFFIX = "_preview"
# Colors for print CLI messages
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
@@ -200,9 +240,63 @@ EXTENDED_ATTRIBUTE_NAMES = [
"authors",
"comment",
"copyright",
"creator",
"description",
"findercomment",
"headline",
"keywords",
"participants",
"projects",
"rating",
"subject",
"title",
"version",
]
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
# name of export DB
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
# bit flags for burst images ("burstPickType")
BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
BURST_SELECTED = 0b1000 # 8: burst image is selected
BURST_KEY = 0b10000 # 16: burst image is the key photo (top of burst stack)
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
LIVE_VIDEO_EXTENSIONS = [".mov"]
# categories that --post-command can be used with; these map to ExportResults fields
POST_COMMAND_CATEGORIES = {
"exported": "All exported files",
"new": "When used with '--update', all newly exported files",
"updated": "When used with '--update', all files which were previously exported but updated this time",
"skipped": "When used with '--update', all files which were skipped (because they were previously exported and didn't change)",
"missing": "All files which were not exported because they were missing from the Photos library",
"exif_updated": "When used with '--exiftool', all files on which exiftool updated the metadata",
"touched": "When used with '--touch-file', all files where the date was touched",
"converted_to_jpeg": "When used with '--convert-to-jpeg', all files which were converted to jpeg",
"sidecar_json_written": "When used with '--sidecar json', all JSON sidecar files which were written",
"sidecar_json_skipped": "When used with '--sidecar json' and '--update', all JSON sidecar files which were skipped",
"sidecar_exiftool_written": "When used with '--sidecar exiftool', all exiftool sidecar files which were written",
"sidecar_exiftool_skipped": "When used with '--sidecar exiftool' and '--update, all exiftool sidecar files which were skipped",
"sidecar_xmp_written": "When used with '--sidecar xmp', all XMP sidecar files which were written",
"sidecar_xmp_skipped": "When used with '--sidecar xmp' and '--update', all XMP sidecar files which were skipped",
"error": "All files which produced an error during export",
# "deleted_files": "When used with '--cleanup', all files deleted during the export",
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
}
class AlbumSortOrder(Enum):
"""Album Sort Order"""
UNKNOWN = 0
MANUAL = 1
NEWEST_FIRST = 2
OLDEST_FIRST = 3
TITLE = 5
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.40.2"
__version__ = "0.44.8"

View File

@@ -0,0 +1,174 @@
""" AdjustmentsInfo class to read adjustments data for photos edited in Apple's Photos.app
In Catalina and Big Sur, the adjustments data (data about edits done to the photo)
is stored in a plist file in
~/Pictures/Photos Library.photoslibrary/resources/renders/X/UUID.plist
where X is first character of the photo's UUID string and UUID is the full UUID,
e.g.: ~/Pictures/Photos Library.photoslibrary/resources/renders/3/30362C1D-192F-4CCD-9A2A-968F436DC0DE.plist
Thanks to @neilpa who figured out how to decode this information:
Reference: https://github.com/neilpa/photohack/issues/4
"""
import datetime
import json
import plistlib
import zlib
from .datetime_utils import datetime_naive_to_utc
class AdjustmentsDecodeError(Exception):
"""Could not decode adjustments plist file"""
def __init__(self, message):
self.message = message
super().__init__(self.message)
class AdjustmentsInfo:
def __init__(self, plist_file):
self._plist_file = plist_file
self._plist = self._load_plist_file(plist_file)
self._base_version = self._plist.get("adjustmentBaseVersion", None)
self._data = self._plist.get("adjustmentData", None)
self._editor_bundle_id = self._plist.get("adjustmentEditorBundleID", None)
self._format_identifier = self._plist.get("adjustmentFormatIdentifier", None)
self._format_version = self._plist.get("adjustmentFormatVersion")
self._timestamp = self._plist.get("adjustmentTimestamp", None)
if self._timestamp and type(self._timestamp) == datetime.datetime:
self._timestamp = datetime_naive_to_utc(self._timestamp)
try:
self._adjustments = self._decode_adjustments_from_plist(self._plist)
except Exception as e:
self._adjustments = None
def _decode_adjustments_from_plist(self, plist):
"""decode adjustmentData from Apple Photos adjustments
Args:
plist: a plist dict as loaded by plistlib
Returns:
decoded adjustmentsData as dict
"""
return json.loads(
zlib.decompress(plist["adjustmentData"], -zlib.MAX_WBITS).decode()
)
def _load_plist_file(self, plist_file):
"""Load plist file from disk
Args:
plist_file: full path to plist file
Returns:
plist as dict
"""
with open(str(plist_file), "rb") as fd:
plist_dict = plistlib.load(fd)
return plist_dict
@property
def plist(self):
"""The actual adjustments plist content as a dict """
return self._plist
@property
def data(self):
"""The raw adjustments data as a binary blob """
return self._data
@property
def editor(self):
"""The editor bundle ID for app/plug-in which made the adjustments """
return self._editor_bundle_id
@property
def format_id(self):
"""The value of the adjustmentFormatIdentifier field in the plist """
return self._format_identifier
@property
def base_version(self):
"""Value of adjustmentBaseVersion field """
return self._base_version
@property
def format_version(self):
"""The value of the adjustmentFormatVersion in the plist """
return self._format_version
@property
def timestamp(self):
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp """
return self._timestamp
@property
def adjustments(self):
"""List of adjustment dictionaries (or empty list if none or could not be decoded)"""
try:
return self._adjustments["adjustments"] if self._adjustments else []
except KeyError:
return []
@property
def adj_metadata(self):
"""Metadata dictionary or None if adjustment data could not be decoded"""
try:
return self._adjustments["metadata"] if self._adjustments else None
except KeyError:
return None
@property
def adj_orientation(self):
"""EXIF orientation of image or 0 if none specified or None if adjustments could not be decoded"""
try:
return self._adjustments["metadata"]["orientation"]
except KeyError:
# no orientation field
return 0
except TypeError:
# adjustments is None
return 0
@property
def adj_format_version(self):
"""Format version for adjustments data (formatVersion field from adjustmentData) or None if adjustments could not be decoded"""
try:
return self._adjustments["formatVersion"] if self._adjustments else None
except KeyError:
return None
@property
def adj_version_info(self):
"""version info for adjustments data or None if adjustments data could not be decoded"""
try:
return self._adjustments["versionInfo"] if self._adjustments else None
except KeyError:
return None
def asdict(self):
"""Returns all adjustments info as dictionary"""
timestamp = self.timestamp
if type(timestamp) == datetime.datetime:
timestamp = timestamp.isoformat()
return {
"data": self.data,
"editor": self.editor,
"format_id": self.format_id,
"base_version": self.base_version,
"format_version": self.format_version,
"adjustments": self.adjustments,
"metadata": self.adj_metadata,
"orientation": self.adj_orientation,
"adjustment_format_version": self.adj_format_version,
"version_info": self.adj_version_info,
"timestamp": timestamp,
}
def __repr__(self):
return f"AdjustmentsInfo(plist_file='{self._plist_file}')"

View File

@@ -14,26 +14,28 @@ 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,
TIME_DELTA,
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .query_builder import get_query
def sort_list_by_keys(values, sort_keys):
""" Sorts list values by a second list sort_keys
"""Sorts list values by a second list sort_keys
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
Args:
values: a list of values to be sorted
sort_keys: a list of keys to sort values by
Returns:
list of values, sorted by sort_keys
Raises:
ValueError: raised if len(values) != len(sort_keys)
"""
@@ -63,12 +65,12 @@ class AlbumInfoBaseClass:
@property
def uuid(self):
""" return uuid of album """
"""return uuid of album"""
return self._uuid
@property
def creation_date(self):
""" return creation date of album """
"""return creation date of album"""
try:
return self._creation_date
except AttributeError:
@@ -90,8 +92,8 @@ class AlbumInfoBaseClass:
@property
def start_date(self):
""" For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began) """
"""For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began)"""
try:
return self._start_date
except AttributeError:
@@ -109,8 +111,8 @@ class AlbumInfoBaseClass:
@property
def end_date(self):
""" For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed) """
"""For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed)"""
try:
return self._end_date
except AttributeError:
@@ -130,43 +132,74 @@ 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 number of photos contained in album"""
return len(self.photos)
class AlbumInfo(AlbumInfoBaseClass):
"""
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
@property
def title(self):
""" return title / name of album """
"""return title / name of album"""
return self._title
@property
def photos(self):
""" return list of photos contained in album sorted in same sort order as Photos """
"""return list of photos contained in album sorted in same sort order as Photos"""
try:
return self._photos
except AttributeError:
if self.uuid in self._db._dbalbums_album:
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
sorted_uuid = sort_list_by_keys(uuid, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
photos = self._db.photos_by_uuid(sorted_uuid)
sort_order = self.sort_order
if sort_order == AlbumSortOrder.NEWEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date, reverse=True)
elif sort_order == AlbumSortOrder.OLDEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date)
elif sort_order == AlbumSortOrder.TITLE:
self._photos = sorted(photos, key=lambda p: p.title or "")
else:
# assume AlbumSortOrder.MANUAL
self._photos = photos
else:
self._photos = []
return self._photos
@property
def folder_names(self):
""" return hierarchical list of folders the album is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
"""return hierarchical list of folders the album is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
try:
return self._folder_names
@@ -176,10 +209,10 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def folder_list(self):
""" return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
"""return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
try:
return self._folders
@@ -189,7 +222,7 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def parent(self):
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """
"""returns FolderInfo object for parent folder or None if no parent (e.g. top-level album)"""
try:
return self._parent
except AttributeError:
@@ -197,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:
@@ -209,11 +242,43 @@ class AlbumInfo(AlbumInfoBaseClass):
)
return self._parent
@property
def sort_order(self):
"""return sort order of album"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return AlbumSortOrder.MANUAL
details = self._db._dbalbum_details[self._uuid]
if details["customsortkey"] == 1:
if details["customsortascending"] == 0:
return AlbumSortOrder.NEWEST_FIRST
elif details["customsortascending"] == 1:
return AlbumSortOrder.OLDEST_FIRST
else:
return AlbumSortOrder.UNKNOWN
elif details["customsortkey"] == 5:
return AlbumSortOrder.TITLE
elif details["customsortkey"] == 0:
return AlbumSortOrder.MANUAL
else:
return AlbumSortOrder.UNKNOWN
def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
for index, p in enumerate(self.photos):
if p.uuid == photo.uuid:
return index
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 """
"""return list of photos contained in import session"""
try:
return self._photos
except AttributeError:
@@ -229,9 +294,18 @@ 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
Info about a specific folder, contains all the details about the folder
including folders, albums, etc
"""
@@ -247,17 +321,17 @@ class FolderInfo:
@property
def title(self):
""" return title / name of folder"""
"""return title / name of folder"""
return self._title
@property
def uuid(self):
""" return uuid of folder """
"""return uuid of folder"""
return self._uuid
@property
def album_info(self):
""" return list of albums (as AlbumInfo objects) contained in the folder """
"""return list of albums (as AlbumInfo objects) contained in the folder"""
try:
return self._albums
except AttributeError:
@@ -282,7 +356,7 @@ class FolderInfo:
@property
def parent(self):
""" returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """
"""returns FolderInfo object for parent or None if no parent (e.g. top-level folder)"""
try:
return self._parent
except AttributeError:
@@ -290,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:
@@ -304,7 +378,7 @@ class FolderInfo:
@property
def subfolders(self):
""" return list of folders (as FolderInfo objects) contained in the folder """
"""return list of folders (as FolderInfo objects) contained in the folder"""
try:
return self._folders
except AttributeError:
@@ -328,5 +402,5 @@ class FolderInfo:
return self._folders
def __len__(self):
""" returns count of folders + albums contained in the folder """
"""returns count of folders + albums contained in the folder"""
return len(self.subfolders) + len(self.album_info)

4333
osxphotos/cli.py Normal file

File diff suppressed because it is too large Load Diff

348
osxphotos/cli_help.py Normal file
View File

@@ -0,0 +1,348 @@
"""Help text helper class for osxphotos CLI """
import io
import pathlib
import re
import click
import osxmetadata
from rich.console import Console
from rich.markdown import Markdown
from ._constants import (
EXTENDED_ATTRIBUTE_NAMES,
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
OSXPHOTOS_EXPORT_DB,
POST_COMMAND_CATEGORIES,
)
from .phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
TEMPLATE_SUBSTITUTIONS_PATHLIB,
get_template_help,
)
# TODO: The following help text could probably be done as mako template
class ExportCommand(click.Command):
"""Custom click.Command that overrides get_help() to show additional help info for export"""
def get_help(self, ctx):
help_text = super().get_help(ctx)
formatter = click.HelpFormatter()
# passed to click.HelpFormatter.write_dl for formatting
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
formatter.write("\n")
formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level "
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
+ "used for determining which files need to be updated when run with --update. It is recommended "
+ "that if you later move the export folder tree you also move the database file."
)
formatter.write("\n")
formatter.write_text(
"The --update option will only copy new or updated files from the library "
+ "to the export folder. If a file is changed in the export folder (for example, you edited the "
+ "exported image), osxphotos will detect this as a difference and re-export the original image "
+ "from the library thus overwriting the changes. If using --update, the exported library "
+ "should be treated as a backup, not a working copy where you intend to make changes. "
+ "If you do edit or process the exported files and do not want them to be overwritten with"
+ "subsequent --update, use --ignore-signature which will match filename but not file signature when "
+ "exporting."
)
formatter.write("\n")
formatter.write_text(
"Note: The number of files reported for export and the number actually exported "
+ "may differ due to live photos, associated raw images, and edited photos which are reported "
+ "in the total photos exported."
)
formatter.write("\n")
formatter.write_text(
"Implementation note: To determine which files need to be updated, "
+ f"osxphotos stores file signature information in the '{OSXPHOTOS_EXPORT_DB}' database. "
+ "The signature includes size, modification time, and filename. In order to minimize "
+ "run time, --update does not do a full comparison (diff) of the files nor does it compare "
+ "hashes of the files. In normal usage, this is sufficient for updating the library. "
+ "You can always run export without the --update option to re-export the entire library thus "
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
)
formatter.write("\n\n")
formatter.write(
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
)
formatter.write("\n")
formatter.write_text(
"""
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
additional metadata to extended attributes in the file. These options will only work
if the destination filesystem supports extended attributes (most do).
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services
do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes
will cause Dropbox to re-sync the files.
The following attributes may be used with '--xattr-template':
"""
)
formatter.write_dl(
[
(
attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
)
for attr in EXTENDED_ATTRIBUTE_NAMES
]
)
formatter.write("\n")
formatter.write_text(
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
)
formatter.write("\n\n")
formatter.write(
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
)
formatter.write("\n")
formatter.write(template_help(width=formatter.width))
formatter.write("\n")
formatter.write_text(
"With the --directory and --filename options you may specify a template for the "
+ "export directory or filename, respectively. "
+ "The directory will be appended to the export path specified "
+ "in the export DEST argument to export. For example, if template is "
+ "'{created.year}/{created.month}', and export destination DEST is "
+ "'/Users/maria/Pictures/export', "
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
+ "if the photo was created in March 2020. "
)
formatter.write("\n")
formatter.write_text(
"The templating system may also be used with the --keyword-template option "
+ "to set keywords on export (with --exiftool or --sidecar), "
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}" '
+ "or in the 'folder>subfolder>album' format used in Lightroom Classic, --keyword-template \"{folder_album(>)}\"."
)
formatter.write("\n")
formatter.write_text(
"In the template, valid template substitutions will be replaced by "
+ "the corresponding value from the table below. Invalid substitutions will result in a "
+ "an error and the script will abort."
)
formatter.write("\n")
formatter.write(
rich_text(
"[bold]** Template Substitutions **[/bold]", width=formatter.width
)
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
formatter.write_dl(templ_tuples)
formatter.write("\n")
formatter.write_text(
"The following substitutions may result in multiple values. Thus "
+ "if specified for --directory these could result in multiple copies of a photo being "
+ "being exported, one to each directory. For example: "
+ "--directory '{created.year}/{album}' could result in the same photo being exported "
+ "to each of the following directories if the photos were created in 2019 "
+ "and were in albums 'Vacation' and 'Family': "
+ "2019/Vacation, 2019/Family"
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend(
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
)
formatter.write_dl(templ_tuples)
formatter.write("\n")
formatter.write_text(
"The following substitutions are file or directory paths. "
+ "You can access various parts of the path using the following modifiers:"
)
formatter.write("\n")
formatter.write("{path.parent}: the parent directory\n")
formatter.write("{path.name}: the name of the file or final sub-directory\n")
formatter.write("{path.stem}: the name of the file without the extension\n")
formatter.write(
"{path.suffix}: the suffix of the file including the leading '.'\n"
)
formatter.write("\n")
formatter.write(
"For example, if the field {export_dir} is '/Shared/Backup/Photos':\n"
)
formatter.write("{export_dir.parent} is '/Shared/Backup'\n")
formatter.write("\n")
formatter.write(
"If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':\n"
)
formatter.write("{filepath.parent} is '/Shared/Backup/Photos'\n")
formatter.write("{filepath.name} is 'IMG_1234.JPG'\n")
formatter.write("{filepath.stem} is 'IMG_1234'\n")
formatter.write("{filepath.suffix} is '.JPG'\n")
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS_PATHLIB.items())
formatter.write_dl(templ_tuples)
formatter.write("\n\n")
formatter.write(
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
)
formatter.write_text(
"You can run commands on the exported photos for post-processing "
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
+ "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: "
)
formatter.write("\n")
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")
formatter.write_text(
"In addition to all normal template fields, the template fields "
+ "'{filepath}' and '{export_dir}' will be available to your command template. "
+ "Both of these are path-type templates which means their various parts can be accessed using "
+ "the available properties, e.g. '{filepath.name}' provides just the file name without path "
+ "and '{filepath.suffix}' is the file extension (suffix) of the file. "
+ "When using paths in your command template, it is important to properly quote the paths "
+ "as they will be passed to the shell and path names may contain spaces. "
+ "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': "
)
formatter.write("\n")
formatter.write(
'--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}' 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: "
)
formatter.write("\n")
formatter.write("echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'")
formatter.write("\n\n")
formatter.write_text(
"It is highly recommended that you run osxphotos with '--dry-run --verbose' "
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
+ "print out the exact command string which would be executed."
)
formatter.write("\n\n")
formatter.write(
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
)
formatter.write_text(
"You can run your own python functions on the exported photos for post-processing "
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
+ "and the name of the function in the file to call using format 'filename.py::function_name'. "
+ "See the example function at https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py "
+ "You may specify multiple functions to run by repeating the --post-function option. "
+ "All post functions will be called immediately after export of each photo and immediately before any --post-command commands. "
+ "Post functions will not be called if the --dry-run flag is set."
)
formatter.write("\n")
help_text += formatter.getvalue()
return help_text
def template_help(width=78):
"""Return formatted string for template system"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
template_help_md = strip_md_header_and_links(get_template_help())
console.print(Markdown(template_help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def tutorial_help(width=78):
"""Return formatted string for tutorial"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
help_md = get_tutorial_text()
help_md = strip_html_comments(help_md)
help_md = strip_md_links(help_md)
console.print(Markdown(help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def rich_text(text, width=78):
"""Return rich formatted text"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
console.print(text)
rich_text = sio.getvalue()
sio.close()
return rich_text
def strip_md_header_and_links(md):
"""strip markdown headers and links from markdown text md
Args:
md: str, markdown text
Returns:
str with markdown headers and links removed
Note: This uses a very basic regex that likely fails on all sorts of edge cases
but works for the links in the osxphotos docs
"""
links = r"(?:[*#])|\[(.*?)\]\(.+?\)"
def subfn(match):
return match.group(1)
return re.sub(links, subfn, md)
def strip_md_links(md):
"""strip markdown links from markdown text md
Args:
md: str, markdown text
Returns:
str with markdown links removed
Note: This uses a very basic regex that likely fails on all sorts of edge cases
but works for the links in the osxphotos docs
"""
links = r"\[(.*?)\]\(.+?\)"
def subfn(match):
return match.group(1)
return re.sub(links, subfn, md)
def strip_html_comments(text):
"""Strip html comments from text (which doesn't need to be valid HTML)"""
return re.sub(r"<!--(.|\s|\n)*?-->", "", text)
def get_tutorial_text():
"""Load tutorial text from file"""
# TODO: would be better to use importlib.abc.ResourceReader but I can't find a single example of how to do this
help_file = pathlib.Path(__file__).parent / "tutorial.md"
with open(help_file, "r") as fd:
md = fd.read()
return md

28
osxphotos/exifinfo.py Normal file
View File

@@ -0,0 +1,28 @@
""" ExifInfo class to expose EXIF info from the library """
from dataclasses import dataclass
@dataclass(frozen=True)
class ExifInfo:
"""EXIF info associated with a photo from the Photos library"""
flash_fired: bool
iso: int
metering_mode: int
sample_rate: int
track_format: int
white_balance: int
aperture: float
bit_rate: float
duration: float
exposure_bias: float
focal_length: float
fps: float
latitude: float
longitude: float
shutter_speed: float
camera_make: str
camera_model: str
codec: str
lens_model: str

View File

@@ -6,22 +6,53 @@
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
import atexit
import html
import json
import logging
import os
import re
import shutil
import subprocess
from abc import ABC, abstractmethod
from functools import lru_cache # pylint: disable=syntax-error
# exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}"
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
# list of exiftool processes to cleanup when exiting or when terminate is called
EXIFTOOL_PROCESSES = []
def escape_str(s):
"""escape string for use with exiftool -E"""
if type(s) != str:
return s
s = html.escape(s)
s = s.replace("\n", "&#xa;")
s = s.replace("\t", "&#x9;")
s = s.replace("\r", "&#xd;")
return s
def unescape_str(s):
"""unescape an HTML string returned by exiftool -E"""
if type(s) != str:
return s
return html.unescape(s)
@atexit.register
def terminate_exiftool():
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""
for proc in EXIFTOOL_PROCESSES:
proc._stop_proc()
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
"""return path of exiftool, cache result"""
exiftool_path = shutil.which("exiftool")
if exiftool_path:
return exiftool_path.rstrip()
@@ -37,7 +68,7 @@ class _ExifToolProc:
Creates a singleton object"""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
@@ -62,24 +93,25 @@ class _ExifToolProc:
@property
def process(self):
""" return the exiftool subprocess """
"""return the exiftool subprocess"""
if self._process_running:
return self._process
else:
raise ValueError("exiftool process is not running")
self._start_proc()
return self._process
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def exiftool(self):
""" return path to exiftool process """
"""return path to exiftool process"""
return self._exiftool
def _start_proc(self):
""" start exiftool in batch mode """
"""start exiftool in batch mode"""
if self._process_running:
logging.warning("exiftool already running: {self._process}")
@@ -97,6 +129,7 @@ class _ExifToolProc:
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-P", # Preserve file modification date/time
"-G", # print group name for each tag
"-E", # escape tag values for HTML (allows use of HTML &#xa; for newlines)
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -104,33 +137,33 @@ class _ExifToolProc:
)
self._process_running = True
EXIFTOOL_PROCESSES.append(self)
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
"""stop the exiftool process if it's running, otherwise, do nothing"""
if not self._process_running:
return
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
try:
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
except Exception as e:
pass
try:
self._process.communicate(timeout=5)
except subprocess.TimeoutExpired:
logging.warning(
f"exiftool pid {self._process.pid} did not exit, killing it"
)
self._process.kill()
self._process.communicate()
del self._process
self._process_running = False
def __del__(self):
self._stop_proc()
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
"""Basic exiftool interface for reading and writing EXIF tags"""
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
"""Create ExifTool object
@@ -153,9 +186,12 @@ class ExifTool:
# if running as a context manager, self._context_mgr will be True
self._context_mgr = False
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
self._process = self._exiftoolproc.process
self._read_exif()
@property
def _process(self):
return self._exiftoolproc.process
def setvalue(self, tag, value):
"""Set tag to value(s); if value is None, will delete tag
@@ -173,15 +209,20 @@ class ExifTool:
if value is None:
value = ""
value = escape_str(value)
command = [f"-{tag}={value}"]
if self.overwrite and not self._context_mgr:
command.append("-overwrite_original")
# avoid "Warning: Some character(s) could not be encoded in Latin" warning
command.append("-iptc:codedcharacterset=utf8")
if self._context_mgr:
self._commands.extend(command)
return True
else:
_, _, error = self.run_commands(*command)
return error is None
return error == ""
def addvalues(self, tag, *values):
"""Add one or more value(s) to tag
@@ -213,6 +254,7 @@ class ExifTool:
for value in values:
if value is None:
raise ValueError("Can't add None value to tag")
value = escape_str(value)
command.append(f"-{tag}+={value}")
if self.overwrite and not self._context_mgr:
@@ -223,7 +265,7 @@ class ExifTool:
return True
else:
_, _, error = self.run_commands(*command)
return error is None
return error == ""
def run_commands(self, *commands, no_file=False):
"""Run commands in the exiftool process and return result.
@@ -254,7 +296,11 @@ class ExifTool:
filename = os.fsencode(self.file) if not no_file else b""
if self.flags:
command_str = b"\n".join([f.encode("utf-8") for f in self.flags])
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
flags = []
for f in self.flags:
flags.extend(f.split())
command_str = b"\n".join([f.encode("utf-8") for f in flags])
command_str += b"\n"
else:
command_str = b""
@@ -291,27 +337,34 @@ class ExifTool:
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def version(self):
""" returns exiftool version """
"""returns exiftool version"""
ver, _, _ = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
def asdict(self, tag_groups=True):
def asdict(self, tag_groups=True, normalized=False):
"""return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
Args:
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
normalized: if True, dict keys are all normalized to lower case (default is False)
"""
json_str, _, _ = self.run_commands("-json")
if not json_str:
return dict()
json_str = unescape_str(json_str.decode("utf-8"))
exifdict = json.loads(json_str)
try:
exifdict = json.loads(json_str)
except Exception as e:
# will fail with some commands, e.g --ext AVI which produces
# 'No file with specified extension' instead of json
return dict()
exifdict = exifdict[0]
if not tag_groups:
# strip tag groups
@@ -320,15 +373,20 @@ class ExifTool:
k = re.sub(r".*:", "", k)
exif_new[k] = v
exifdict = exif_new
if normalized:
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
return exifdict
def json(self):
""" returns JSON string containing all EXIF tags and values from exiftool """
"""returns JSON string containing all EXIF tags and values from exiftool"""
json, _, _ = self.run_commands("-json")
json = unescape_str(json.decode("utf-8"))
return json
def _read_exif(self):
""" read exif data from file """
"""read exif data from file"""
data = self.asdict()
self.data = {k: v for k, v in data.items()}
@@ -346,3 +404,73 @@ class ExifTool:
elif self._commands:
# run_commands sets self.warning and self.error as needed
self.run_commands(*self._commands)
class ExifToolCaching(ExifTool):
"""Basic exiftool interface for reading and writing EXIF tags, with caching.
Use this only when you know the file's EXIF data will not be changed by any external process.
Creates a singleton cached ExifTool instance"""
_singletons = {}
def __new__(cls, filepath, exiftool=None):
"""create new object or return instance of already created singleton"""
if filepath not in cls._singletons:
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
return cls._singletons[filepath]
class _ExifToolCaching(ExifTool):
def __init__(self, filepath, exiftool=None):
"""Create read-only ExifTool object that caches values
Args:
file: path to image file
exiftool: path to exiftool, if not specified will look in path
Returns:
ExifTool instance
"""
self._json_cache = None
self._asdict_cache = {}
super().__init__(filepath, exiftool=exiftool, overwrite=False, flags=None)
def run_commands(self, *commands, no_file=False):
if commands[0] not in ["-json", "-ver"]:
raise NotImplementedError(f"{self.__class__} is read-only")
return super().run_commands(*commands, no_file=no_file)
def setvalue(self, tag, value):
raise NotImplementedError(f"{self.__class__} is read-only")
def addvalues(self, tag, *values):
raise NotImplementedError(f"{self.__class__} is read-only")
def json(self):
if not self._json_cache:
self._json_cache = super().json()
return self._json_cache
def asdict(self, tag_groups=True, normalized=False):
"""return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
Args:
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
normalized: if True, dict keys are all normalized to lower case (default is False)
"""
try:
return self._asdict_cache[tag_groups][normalized]
except KeyError:
if tag_groups not in self._asdict_cache:
self._asdict_cache[tag_groups] = {}
self._asdict_cache[tag_groups][normalized] = super().asdict(
tag_groups=tag_groups, normalized=normalized
)
return self._asdict_cache[tag_groups][normalized]
def flush_cache(self):
"""Clear cached data so that calls to json or asdict return fresh data"""
self._json_cache = None
self._asdict_cache = {}

View File

@@ -1,6 +1,5 @@
""" Helper class for managing a database used by
PhotoInfo.export for tracking state of exports and updates
"""
""" Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
import datetime
import logging
@@ -12,13 +11,15 @@ from abc import ABC, abstractmethod
from io import StringIO
from sqlite3 import Error
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
class ExportDB_ABC(ABC):
""" abstract base class for ExportDB """
"""abstract base class for ExportDB"""
@abstractmethod
def get_uuid_for_file(self, filename):
@@ -84,6 +85,18 @@ class ExportDB_ABC(ABC):
def get_sidecar_for_file(self, filename):
pass
@abstractmethod
def get_previous_uuids(self):
pass
@abstractmethod
def get_detected_text_for_uuid(self, uuid):
pass
@abstractmethod
def set_detected_text_for_uuid(self, uuid, json_text):
pass
@abstractmethod
def set_data(
self,
@@ -100,7 +113,7 @@ class ExportDB_ABC(ABC):
class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """
"""An ExportDB with NoOp methods"""
def __init__(self):
self.was_created = True
@@ -155,6 +168,15 @@ class ExportDBNoOp(ExportDB_ABC):
def get_sidecar_for_file(self, filename):
return None, (None, None, None)
def get_previous_uuids(self):
return []
def get_detected_text_for_uuid(self, uuid):
return None
def set_detected_text_for_uuid(self, uuid, json_text):
pass
def set_data(
self,
filename,
@@ -170,58 +192,57 @@ class ExportDBNoOp(ExportDB_ABC):
class ExportDB(ExportDB_ABC):
""" Interface to sqlite3 database used to store state information for osxphotos export command """
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
def __init__(self, dbfile):
""" dbfile: path to osxphotos export database file """
def __init__(self, dbfile, export_dir):
"""dbfile: path to osxphotos export database file"""
self._dbfile = dbfile
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
self._path = pathlib.Path(dbfile).parent
# whilst preserving the UUID to filename mapping
self._path = export_dir
self._conn = self._open_export_db(dbfile)
self._insert_run_info()
def get_uuid_for_file(self, filename):
""" query database for filename and return UUID
returns None if filename not found in database
"""query database for filename and return UUID
returns None if filename not found in database
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
c = conn.cursor()
c.execute(
f"SELECT uuid FROM files WHERE filepath_normalized = ?", (filename,)
"SELECT uuid FROM files WHERE filepath_normalized = ?", (filename,)
)
results = c.fetchone()
uuid = results[0] if results else None
except Error as e:
logging.warning(e)
uuid = None
return uuid
def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """
"""set UUID of filename to uuid in the database"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
try:
c = conn.cursor()
c.execute(
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
(filename, filename_normalized, uuid),
)
conn.commit()
except Error as e:
logging.warning(e)
def set_stat_orig_for_file(self, filename, stats):
""" set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -240,8 +261,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_orig_for_file(self, filename):
""" get stat info for filename
returns: tuple of (mode, size, mtime)
"""get stat info for filename
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -253,33 +274,32 @@ class ExportDB(ExportDB_ABC):
)
results = c.fetchone()
if results:
stats = results[0:3]
stats = results[:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
stats = (None, None, None)
except Error as e:
logging.warning(e)
stats = (None, None, None)
stats = None, None, None
return stats
def set_stat_edited_for_file(self, filename, stats):
""" set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("edited", filename, stats)
def get_stat_edited_for_file(self, filename):
""" get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._get_stat_for_file("edited", filename)
def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -298,8 +318,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_exif_for_file(self, filename):
""" get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -311,31 +331,30 @@ class ExportDB(ExportDB_ABC):
)
results = c.fetchone()
if results:
stats = results[0:3]
stats = results[:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
stats = (None, None, None)
except Error as e:
logging.warning(e)
stats = (None, None, None)
stats = None, None, None
return stats
def set_stat_converted_for_file(self, filename, stats):
""" set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("converted", filename, stats)
def get_stat_converted_for_file(self, filename):
""" get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""
return self._get_stat_for_file("converted", filename)
def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """
"""returns the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -349,7 +368,7 @@ class ExportDB(ExportDB_ABC):
return info
def set_info_for_uuid(self, uuid, info):
""" sets the info JSON struct for a UUID """
"""sets the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -362,7 +381,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """
"""returns the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -380,7 +399,7 @@ class ExportDB(ExportDB_ABC):
return exifdata
def set_exifdata_for_file(self, filename, exifdata):
""" sets the exifdata JSON struct for a file """
"""sets the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -394,7 +413,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_sidecar_for_file(self, filename):
""" returns the sidecar data and signature for a file """
"""returns the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -422,7 +441,7 @@ class ExportDB(ExportDB_ABC):
return sidecar_data, sidecar_sig
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
""" sets the sidecar data and signature for a file """
"""sets the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -435,6 +454,52 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database"""
conn = self._conn
previous_uuids = []
try:
c = conn.cursor()
c.execute("SELECT DISTINCT uuid FROM files")
results = c.fetchall()
previous_uuids = [row[0] for row in results]
except Error as e:
logging.warning(e)
return previous_uuids
def get_detected_text_for_uuid(self, uuid):
"""Get the detected_text for a uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT text_data FROM detected_text WHERE uuid = ?",
(uuid,),
)
results = c.fetchone()
detected_text = results[0] if results else None
except Error as e:
logging.warning(e)
detected_text = None
return detected_text
def set_detected_text_for_uuid(self, uuid, text_json):
"""Set the detected text for uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
(
uuid,
text_json,
),
)
conn.commit()
except Error as e:
logging.warning(e)
def set_data(
self,
filename,
@@ -446,17 +511,17 @@ class ExportDB(ExportDB_ABC):
info_json,
exif_json,
):
""" sets all the data for file and uuid at once
"""
"""sets all the data for file and uuid at once"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
try:
c = conn.cursor()
c.execute(
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
(filename, filename_normalized, uuid),
)
c.execute(
"UPDATE files "
+ "SET orig_mode = ?, orig_size = ?, orig_mtime = ? "
@@ -490,7 +555,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def close(self):
""" close the database connection """
"""close the database connection"""
try:
self._conn.close()
except Error as e:
@@ -519,7 +584,7 @@ class ExportDB(ExportDB_ABC):
)
results = c.fetchone()
if results:
stats = results[0:3]
stats = results[:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
@@ -528,9 +593,9 @@ class ExportDB(ExportDB_ABC):
return stats
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""
if not os.path.isfile(dbfile):
@@ -553,7 +618,7 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_db_connection(self, dbfile):
""" return db connection to dbname """
"""return db connection to dbname"""
try:
conn = sqlite3.connect(dbfile)
except Error as e:
@@ -563,15 +628,15 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_database_version(self, conn):
""" return tuple of (osxphotos, exportdb) versions for database connection conn """
"""return tuple of (osxphotos, exportdb) versions for database connection conn"""
version_info = conn.execute(
"SELECT osxphotos, exportdb, max(id) FROM version"
).fetchone()
return (version_info[0], version_info[1])
def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""
sql_commands = {
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
@@ -579,6 +644,10 @@ class ExportDB(ExportDB_ABC):
osxphotos TEXT,
exportdb TEXT
); """,
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
id INTEGER PRIMARY KEY,
about TEXT
);""",
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY,
filepath TEXT NOT NULL,
@@ -631,12 +700,18 @@ class ExportDB(ExportDB_ABC):
size INTEGER,
mtime REAL
); """,
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
text_data JSON
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
}
try:
c = conn.cursor()
@@ -646,12 +721,13 @@ class ExportDB(ExportDB_ABC):
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
)
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit()
except Error as e:
logging.warning(e)
def __del__(self):
""" ensure the database connection is closed """
"""ensure the database connection is closed"""
try:
self._conn.close()
except:
@@ -667,44 +743,42 @@ class ExportDB(ExportDB_ABC):
try:
c = conn.cursor()
c.execute(
f"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
(dt, python_path, cmd, args, cwd),
)
conn.commit()
except Error as e:
logging.warning(e)
class ExportDBInMemory(ExportDB):
""" In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk verison
"""In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk version
"""
def init(self, dbfile):
self._dbfile = dbfile
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
def __init__(self, dbfile, export_dir):
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile)
# whilst preserving the UUID to filename mapping
self._path = export_dir
self._conn = self._open_export_db(self._dbfile)
self._insert_run_info()
def _open_export_db(self, dbfile):
""" open export database and return a db connection
returns: connection to the database
"""open export database and return a db connection
returns: connection to the database
"""
if not os.path.isfile(dbfile):
conn = self._get_db_connection()
if conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
if not conn:
raise Exception("Error getting connection to in-memory database")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else:
try:
conn = sqlite3.connect(dbfile)
@@ -729,12 +803,11 @@ class ExportDBInMemory(ExportDB):
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
def _get_db_connection(self):
""" return db connection to in memory database """
"""return db connection to in memory database"""
try:
conn = sqlite3.connect(":memory:")
except Error as e:

View File

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

View File

@@ -17,25 +17,25 @@ from wurlitzer import pipes
class ImageConversionError(Exception):
"""Base class for exceptions in this module. """
"""Base class for exceptions in this module."""
pass
class ImageConverter:
""" Convert images to jpeg. This class is a singleton
which will re-use the Core Image CIContext to avoid
creating a new context for every conversion. """
"""Convert images to jpeg. This class is a singleton
which will re-use the Core Image CIContext to avoid
creating a new context for every conversion."""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
return cls.instance
def __init__(self):
""" return existing singleton or create a new one """
"""return existing singleton or create a new one"""
if hasattr(self, "context"):
return
@@ -47,13 +47,10 @@ class ImageConverter:
"workingFormat": Quartz.kCIFormatRGBAh,
}
)
mtldevice = Metal.MTLCreateSystemDefaultDevice()
self.context = Quartz.CIContext.contextWithMTLDevice_options_(
mtldevice, context_options
)
self.context = Quartz.CIContext.contextWithOptions_(context_options)
def write_jpeg(self, input_path, output_path, compression_quality=1.0):
""" convert image to jpeg and write image to output_path
"""convert image to jpeg and write image to output_path
Args:
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
@@ -104,8 +101,11 @@ class ImageConverter:
if input_image is None:
raise ImageConversionError(f"Could not create CIImage for {input_path}")
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
Quartz.CoreGraphics.kCGColorSpaceSRGB
output_colorspace = (
input_image.colorSpace()
or Quartz.CGColorSpaceCreateWithName(
Quartz.CoreGraphics.kCGColorSpaceSRGB
)
)
output_options = NSDictionary.dictionaryWithDictionary_(
@@ -121,6 +121,5 @@ class ImageConverter:
return True
else:
raise ImageConversionError(
"Error converting file {input_path} to jpeg at {output_path}: {error}"
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
)

69
osxphotos/momentinfo.py Normal file
View File

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

View File

@@ -6,22 +6,22 @@ from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
def sanitize_filepath(filepath):
""" sanitize a filepath """
"""sanitize a filepath"""
return pathvalidate.sanitize_filepath(filepath, platform="macos")
def is_valid_filepath(filepath):
""" returns True if a filepath is valid otherwise False """
"""returns True if a filepath is valid otherwise False"""
return pathvalidate.is_valid_filepath(filepath, platform="macos")
def sanitize_filename(filename, replacement=":"):
""" replace any illegal characters in a filename and truncate filename if needed
"""replace any illegal characters in a filename and truncate filename if needed
Args:
filename: str, filename to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
Returns:
filename with any illegal characters replaced by replacement and truncated if necessary
"""
@@ -46,12 +46,12 @@ def sanitize_filename(filename, replacement=":"):
def sanitize_dirname(dirname, replacement=":"):
""" replace any illegal characters in a directory name and truncate directory name if needed
"""replace any illegal characters in a directory name and truncate directory name if needed
Args:
dirname: str, directory name to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
dirname: str, directory name to sanitize
replacement: str, value to replace any illegal characters with; default = ":"; if None, no replacement occurs
Returns:
dirname with any illegal characters replaced by replacement and truncated if necessary
"""
@@ -61,19 +61,20 @@ def sanitize_dirname(dirname, replacement=":"):
def sanitize_pathpart(pathpart, replacement=":"):
""" replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
"""replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
Args:
pathpart: str, path part to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
pathpart: str, path part to sanitize
replacement: str, value to replace any illegal characters with; default = ":"; if None, no replacement occurs
Returns:
pathpart with any illegal characters replaced by replacement and truncated if necessary
"""
if pathpart:
pathpart = pathpart.replace("/", replacement)
pathpart = (
pathpart.replace("/", replacement) if replacement is not None else pathpart
)
if len(pathpart) > MAX_DIRNAME_LEN:
drop = len(pathpart) - MAX_DIRNAME_LEN
pathpart = pathpart[:-drop]
return pathpart

View File

@@ -11,16 +11,15 @@ MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
class PersonInfo:
""" Info about a person in the Photos library
"""
"""Info about a person in the Photos library"""
def __init__(self, db=None, pk=None):
""" Creates a new PersonInfo instance
"""Creates a new PersonInfo instance
Arguments:
db: instance of PhotosDB object
pk: primary key value of person to initialize PersonInfo with
pk: primary key value of person to initialize PersonInfo with
Returns:
PersonInfo instance
"""
@@ -57,8 +56,8 @@ class PersonInfo:
@property
def face_info(self):
""" Returns a list of FaceInfo objects associated with this person sorted by quality score
Highest quality face is result[0] and lowest quality face is result[n]
"""Returns a list of FaceInfo objects associated with this person sorted by quality score
Highest quality face is result[0] and lowest quality face is result[n]
"""
try:
faces = self._db._db_faceinfo_person[self._pk]
@@ -103,16 +102,15 @@ class PersonInfo:
class FaceInfo:
""" Info about a face in the Photos library
"""
"""Info about a face in the Photos library"""
def __init__(self, db=None, pk=None):
""" Creates a new FaceInfo instance
"""Creates a new FaceInfo instance
Arguments:
db: instance of PhotosDB object
pk: primary key value of face to init the object with
pk: primary key value of face to init the object with
Returns:
FaceInfo instance
"""
@@ -143,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"]
@@ -156,7 +154,7 @@ class FaceInfo:
@property
def center(self):
""" Coordinates, in PIL format, for center of face
"""Coordinates, in PIL format, for center of face
Returns:
tuple of coordinates in form (x, y)
@@ -165,7 +163,7 @@ class FaceInfo:
@property
def size_pixels(self):
""" Size of face in pixels (centered around center_x, center_y)
"""Size of face in pixels (centered around center_x, center_y)
Returns:
size, in int pixels, of a circle drawn around the center of the face
@@ -176,7 +174,7 @@ class FaceInfo:
@property
def mouth(self):
""" Coordinates, in PIL format, for mouth position
"""Coordinates, in PIL format, for mouth position
Returns:
tuple of coordinates in form (x, y)
@@ -185,7 +183,7 @@ class FaceInfo:
@property
def left_eye(self):
""" Coordinates, in PIL format, for left eye position
"""Coordinates, in PIL format, for left eye position
Returns:
tuple of coordinates in form (x, y)
@@ -194,7 +192,7 @@ class FaceInfo:
@property
def right_eye(self):
""" Coordinates, in PIL format, for right eye position
"""Coordinates, in PIL format, for right eye position
Returns:
tuple of coordinates in form (x, y)
@@ -223,7 +221,7 @@ class FaceInfo:
@property
def mwg_rs_area(self):
""" Get coordinates for Metadata Working Group Region Area.
"""Get coordinates for Metadata Working Group Region Area.
Returns:
MWG_RS_Area named tuple with x, y, h, w where:
@@ -249,7 +247,7 @@ class FaceInfo:
@property
def mpri_reg_rect(self):
""" Get coordinates for Microsoft Photo Region Rectangle.
"""Get coordinates for Microsoft Photo Region Rectangle.
Returns:
MPRI_Reg_Rect named tuple with x, y, h, w where:
@@ -278,7 +276,7 @@ class FaceInfo:
return MPRI_Reg_Rect(x, y, h, w)
def face_rect(self):
""" Get face rectangle coordinates for current version of the associated image
"""Get face rectangle coordinates for current version of the associated image
If image has been edited, rectangle applies to edited version, otherwise original version
Coordinates in format and reference frame used by PIL
@@ -321,12 +319,12 @@ class FaceInfo:
return yaw
def _fix_orientation(self, xy):
""" Translate an (x, y) tuple based on image orientation
"""Translate an (x, y) tuple based on image orientation
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates
"""
@@ -350,21 +348,24 @@ class FaceInfo:
elif orientation == 7:
x, y = y, x
y = 1.0 - y
elif orientation ==8:
elif orientation == 8:
x, y = y, x
elif orientation == 0:
# set by osxphotos if adjusted orientation cannot be read, assume it's 1
y = 1.0 - y
else:
logging.warning(f"Unhandled orientation: {orientation}")
return (x, y)
def _make_point(self, xy):
""" Translate an (x, y) tuple based on image orientation
"""Translate an (x, y) tuple based on image orientation
and convert to image coordinates
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame
"""
@@ -379,13 +380,13 @@ class FaceInfo:
return (int(x * dx), int(y * dy))
def _make_point_with_rotation(self, xy):
""" Translate an (x, y) tuple based on image orientation and rotation
"""Translate an (x, y) tuple based on image orientation and rotation
and convert to image coordinates
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame
"""
@@ -437,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,
@@ -472,14 +473,14 @@ class FaceInfo:
def rotate_image_point(x, y, xmid, ymid, angle):
""" rotate image point about xm, ym by angle in radians
"""rotate image point about xm, ym by angle in radians
Arguments:
x: x coordinate of point to rotate
x: x coordinate of point to rotate
y: y coordinate of point to rotate
xmid: x coordinate of center point to rotate about
ymid: y coordinate of center point to rotate about
angle: angle in radians about which to coordinate,
angle: angle in radians about which to coordinate,
counter-clockwise is positive
Returns:

2135
osxphotos/photoexporter.py Normal file

File diff suppressed because it is too large Load Diff

1737
osxphotos/photoinfo.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
"""
PhotoInfo class
Represents a single photo in the Photos library and provides access to the photo's attributes
PhotosDB.photos() returns a list of PhotoInfo objects
"""
from ._photoinfo_exifinfo import ExifInfo
from ._photoinfo_export import ExportResults
from ._photoinfo_scoreinfo import ScoreInfo
from .photoinfo import PhotoInfo

View File

@@ -1,17 +0,0 @@
""" PhotoInfo methods to expose comments and likes for shared photos """
@property
def comments(self):
""" Returns list of Comment objects for any comments on the photo (sorted by date) """
try:
return self._db._db_comments_uuid[self.uuid]["comments"]
except:
return []
@property
def likes(self):
""" Returns list of Like objects for any likes on the photo (sorted by date) """
try:
return self._db._db_comments_uuid[self.uuid]["likes"]
except:
return []

View File

@@ -1,94 +0,0 @@
""" PhotoInfo methods to expose EXIF info from the library """
import logging
from dataclasses import dataclass
from .._constants import _PHOTOS_4_VERSION
@dataclass(frozen=True)
class ExifInfo:
""" EXIF info associated with a photo from the Photos library """
flash_fired: bool
iso: int
metering_mode: int
sample_rate: int
track_format: int
white_balance: int
aperture: float
bit_rate: float
duration: float
exposure_bias: float
focal_length: float
fps: float
latitude: float
longitude: float
shutter_speed: float
camera_make: str
camera_model: str
codec: str
lens_model: str
@property
def exif_info(self):
""" Returns an ExifInfo object with the EXIF data for photo
Note: the returned EXIF data is the data Photos stores in the database on import;
ExifInfo does not provide access to the EXIF info in the actual image file
Some or all of the fields may be None
Only valid for Photos 5; on earlier database returns None
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"exif_info not implemented for this database version")
return None
try:
exif = self._db._db_exifinfo_uuid[self.uuid]
exif_info = ExifInfo(
iso=exif["ZISO"],
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
metering_mode=exif["ZMETERINGMODE"],
sample_rate=exif["ZSAMPLERATE"],
track_format=exif["ZTRACKFORMAT"],
white_balance=exif["ZWHITEBALANCE"],
aperture=exif["ZAPERTURE"],
bit_rate=exif["ZBITRATE"],
duration=exif["ZDURATION"],
exposure_bias=exif["ZEXPOSUREBIAS"],
focal_length=exif["ZFOCALLENGTH"],
fps=exif["ZFPS"],
latitude=exif["ZLATITUDE"],
longitude=exif["ZLONGITUDE"],
shutter_speed=exif["ZSHUTTERSPEED"],
camera_make=exif["ZCAMERAMAKE"],
camera_model=exif["ZCAMERAMODEL"],
codec=exif["ZCODEC"],
lens_model=exif["ZLENSMODEL"],
)
except KeyError:
logging.debug(f"Could not find exif record for uuid {self.uuid}")
exif_info = ExifInfo(
iso=None,
flash_fired=None,
metering_mode=None,
sample_rate=None,
track_format=None,
white_balance=None,
aperture=None,
bit_rate=None,
duration=None,
exposure_bias=None,
focal_length=None,
fps=None,
latitude=None,
longitude=None,
shutter_speed=None,
camera_make=None,
camera_model=None,
codec=None,
lens_model=None,
)
return exif_info

View File

@@ -1,34 +0,0 @@
""" Implementation for PhotoInfo.exiftool property which returns ExifTool object for a photo """
import logging
import os
from ..exiftool import ExifTool, get_exiftool_path
@property
def exiftool(self):
""" Returns an ExifTool object for the photo
requires that exiftool (https://exiftool.org/) be installed
If exiftool not installed, logs warning and returns None
If photo path is missing, returns None
"""
try:
# return the memoized instance if it exists
return self._exiftool
except AttributeError:
try:
exiftool_path = self._db._exiftool_path or get_exiftool_path()
if self.path is not None and os.path.isfile(self.path):
exiftool = ExifTool(self.path, exiftool=exiftool_path)
else:
exiftool = None
except FileNotFoundError:
# get_exiftool_path raises FileNotFoundError if exiftool not found
exiftool = None
logging.warning(
f"exiftool not in path; download and install from https://exiftool.org/"
)
self._exiftool = exiftool
return self._exiftool

File diff suppressed because it is too large Load Diff

View File

@@ -1,119 +0,0 @@
""" PhotoInfo methods to expose computed score info from the library """
import logging
from dataclasses import dataclass
from .._constants import _PHOTOS_4_VERSION
@dataclass(frozen=True)
class ScoreInfo:
""" Computed photo score info associated with a photo from the Photos library """
overall: float
curation: float
promotion: float
highlight_visibility: float
behavioral: float
failure: float
harmonious_color: float
immersiveness: float
interaction: float
interesting_subject: float
intrusive_object_presence: float
lively_color: float
low_light: float
noise: float
pleasant_camera_tilt: float
pleasant_composition: float
pleasant_lighting: float
pleasant_pattern: float
pleasant_perspective: float
pleasant_post_processing: float
pleasant_reflection: float
pleasant_symmetry: float
sharply_focused_subject: float
tastefully_blurred: float
well_chosen_subject: float
well_framed_subject: float
well_timed_shot: float
@property
def score(self):
""" Computed score information for a photo
Returns:
ScoreInfo instance
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"score not implemented for this database version")
return None
try:
return self._scoreinfo # pylint: disable=access-member-before-definition
except AttributeError:
try:
scores = self._db._db_scoreinfo_uuid[self.uuid]
self._scoreinfo = ScoreInfo(
overall=scores["overall_aesthetic"],
curation=scores["curation"],
promotion=scores["promotion"],
highlight_visibility=scores["highlight_visibility"],
behavioral=scores["behavioral"],
failure=scores["failure"],
harmonious_color=scores["harmonious_color"],
immersiveness=scores["immersiveness"],
interaction=scores["interaction"],
interesting_subject=scores["interesting_subject"],
intrusive_object_presence=scores["intrusive_object_presence"],
lively_color=scores["lively_color"],
low_light=scores["low_light"],
noise=scores["noise"],
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
pleasant_composition=scores["pleasant_composition"],
pleasant_lighting=scores["pleasant_lighting"],
pleasant_pattern=scores["pleasant_pattern"],
pleasant_perspective=scores["pleasant_perspective"],
pleasant_post_processing=scores["pleasant_post_processing"],
pleasant_reflection=scores["pleasant_reflection"],
pleasant_symmetry=scores["pleasant_symmetry"],
sharply_focused_subject=scores["sharply_focused_subject"],
tastefully_blurred=scores["tastefully_blurred"],
well_chosen_subject=scores["well_chosen_subject"],
well_framed_subject=scores["well_framed_subject"],
well_timed_shot=scores["well_timed_shot"],
)
return self._scoreinfo
except KeyError:
self._scoreinfo = ScoreInfo(
overall=0.0,
curation=0.0,
promotion=0.0,
highlight_visibility=0.0,
behavioral=0.0,
failure=0.0,
harmonious_color=0.0,
immersiveness=0.0,
interaction=0.0,
interesting_subject=0.0,
intrusive_object_presence=0.0,
lively_color=0.0,
low_light=0.0,
noise=0.0,
pleasant_camera_tilt=0.0,
pleasant_composition=0.0,
pleasant_lighting=0.0,
pleasant_pattern=0.0,
pleasant_perspective=0.0,
pleasant_post_processing=0.0,
pleasant_reflection=0.0,
pleasant_symmetry=0.0,
sharply_focused_subject=0.0,
tastefully_blurred=0.0,
well_chosen_subject=0.0,
well_framed_subject=0.0,
well_timed_shot=0.0,
)
return self._scoreinfo

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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