Compare commits

...

282 Commits

Author SHA1 Message Date
Rhet Turnbull
5b25284db2 Added docs command, partial for #666 2022-04-19 10:07:33 -07:00
Rhet Turnbull
91784ab982 Added osxphotos/docs 2022-04-19 09:54:27 -07:00
Rhet Turnbull
5ea708f063 Added osxphotos/docs 2022-04-19 09:47:07 -07:00
Rhet Turnbull
840a252083 Added docs 2022-04-19 09:40:08 -07:00
Rhet Turnbull
0a83e14210 Feature docs (#665)
* Added docs command

* Version bump

* Updated sphinx docs
2022-04-19 09:23:31 -07:00
Rhet Turnbull
afe5ed3dc0 Added validation for template string options 2022-04-18 10:28:02 -07:00
Rhet Turnbull
c48887612c Updated CHANGELOG.md [skip ci] 2022-04-17 23:16:40 -07:00
Rhet Turnbull
b09323b9fb Updated docs [skip ci] 2022-04-17 22:56:53 -07:00
Rhet Turnbull
213d84e964 Version bump 2022-04-17 22:54:33 -07:00
Rhet Turnbull
6c57fb2df9 Theme (#664)
* Initial theme manager, not yet done

* Added rich_theme_manager

* Updated rich-theme-manager

* Switched to rich_theme_manager for theme management

* Updated dependencies

* Added rich paging to subtopic help

* Fixed clone to clone only styles specified in cloned theme

* Added placeholder for help colors

* Updated config dir, help methods
2022-04-17 22:53:42 -07:00
Rhet Turnbull
9c0b910046 Added cov.xml [skip ci] 2022-04-03 09:03:55 -07:00
Rhet Turnbull
1f40161950 Fixed typing in examples 2022-04-01 18:16:23 -07:00
Rhet Turnbull
d1aa4e92bd Quoted path in repl 2022-03-29 06:13:57 -07:00
Rhet Turnbull
e7a17a8635 Updated CHANGELOG.md [skip ci] 2022-03-27 17:50:19 -07:00
Rhet Turnbull
68754273de Updated docs [skip ci] 2022-03-27 17:34:38 -07:00
Rhet Turnbull
d28a2fe9bb version bump 2022-03-27 09:53:03 -07:00
Rhet Turnbull
382d097285 fix verbose output when redirected to file, #661 2022-03-27 09:52:23 -07:00
Rhet Turnbull
93de53da51 Updated CHANGELOG.md [skip ci] 2022-03-12 10:00:48 -08:00
Rhet Turnbull
e272e95a85 Fixed missing pdb.py issue for pyinstaller, partial for #659 2022-03-12 09:00:41 -08:00
Rhet Turnbull
84a96bd4d0 Cleaned up fileutil, rolled back changes for #654 2022-03-11 06:06:21 -08:00
Rhet Turnbull
d26b625d57 Cleaned up fileutil, rolled back changes for #654 2022-03-11 06:04:21 -08:00
Rhet Turnbull
8731e7d5bc Hack to fix #654 when utime fails on NAS 2022-03-09 21:24:56 -08:00
Rhet Turnbull
2e501e6a9b Added run command which had gotten dropped, #656 2022-03-09 07:10:51 -08:00
Rhet Turnbull
7f4c981abe Added --no-progress, #655 2022-03-09 07:08:34 -08:00
Rhet Turnbull
bbcc3acba9 Changed return val of _should_update_photo to enum for easier debugging 2022-03-09 06:52:17 -08:00
Rhet Turnbull
fccd746c58 Updated docs [skip ci] 2022-03-06 07:20:57 -08:00
Rhet Turnbull
adb90a3364 Version bump 2022-03-06 07:17:53 -08:00
Rhet Turnbull
445010e7e5 Richify (#653)
* Improved rich_echo, added rich_echo_via_pager

* Initial implementation for #647, added rich output
2022-03-06 07:17:09 -08:00
Rhet Turnbull
1227465aa7 Updated crash_reporter to include crash data 2022-03-04 20:26:19 -08:00
Rhet Turnbull
de1900f10a Debug updates 2022-03-04 20:05:15 -08:00
Rhet Turnbull
ed315fffd2 Added --watch, --breakpoint (#652) 2022-03-04 06:45:57 -08:00
Rhet Turnbull
be1f3a98d9 Updated CHANGELOG.md [skip ci] 2022-03-02 07:09:53 -08:00
Rhet Turnbull
d8802368fc Added --tmpdir, #650 (#651) 2022-03-02 06:58:23 -08:00
Rhet Turnbull
f132e9a843 Version bump 2022-02-27 20:30:22 -08:00
Rhet Turnbull
6b342a1733 Version bump 2022-02-27 20:29:23 -08:00
Rhet Turnbull
9dec028448 Help topic (#644)
* Initial implementation for #607

* Implemented #607, add help for sub topics

* Updated test workflow
2022-02-27 16:53:11 -08:00
Rhet Turnbull
8be6a98c32 Added -v to pytest 2022-02-27 16:39:12 -08:00
Rhet Turnbull
ce73c9cab8 updated docs [skip ci] 2022-02-27 14:14:52 -08:00
Rhet Turnbull
feb9538d1c Fix for --load-config, #643 2022-02-27 09:50:25 -08:00
Rhet Turnbull
b275280a1f Updated README.md 2022-02-26 23:25:16 -08:00
Rhet Turnbull
924ef72446 Updated CHANGELOG.md [skip ci] 2022-02-26 23:03:07 -08:00
Rhet Turnbull
c95f682ca6 Updated docs [skip ci] 2022-02-26 22:57:25 -08:00
Rhet Turnbull
d2753672f3 Fixed entry point 2022-02-26 22:49:39 -08:00
Rhet Turnbull
8ee4ea46c5 Updated CHANGELOG.md [skip ci] 2022-02-26 22:49:18 -08:00
Rhet Turnbull
6fae979061 Updated docs [skip ci] 2022-02-26 22:34:03 -08:00
Rhet Turnbull
25d6f148be CLI refactor (#642)
* Initial refactoring of cli.py

* Renamed cli_help

* Refactored all cli commands

* Dropped support for 3.7

* Added test for export with --min-size

* Version bump

* Fixed python version
2022-02-26 22:29:19 -08:00
Rhet Turnbull
3704fc4a23 Fixed 3.10 in yaml 2022-02-26 17:20:20 -08:00
Rhet Turnbull
7883fc1911 Dropped 3.7 2022-02-26 17:15:59 -08:00
Rhet Turnbull
29ff7f8666 Updated CHANGELOG.md [skip ci] 2022-02-26 16:51:51 -08:00
Rhet Turnbull
43e1cb18cc Updated tests 2022-02-26 15:30:13 -08:00
Rhet Turnbull
26f916e4cb Bug fix for bitmath types in saved config 2022-02-26 15:23:46 -08:00
Rhet Turnbull
4e9e877b27 Updated CHANGELOG.md [skip ci] 2022-02-24 05:23:49 -08:00
Rhet Turnbull
3a990e3997 Updated docs [skip ci] 2022-02-24 05:22:58 -08:00
Rhet Turnbull
4d1b1db2a7 Updated tested versions 2022-02-24 05:18:23 -08:00
Rhet Turnbull
173e3ccc37 Removed debug code from exiftool, fixed #641 2022-02-24 05:09:42 -08:00
Rhet Turnbull
9964fd0635 Updated comment for #636 2022-02-23 06:15:03 -08:00
Rhet Turnbull
e789cd5e9d pass PATH to exiftool to find xattr 2022-02-22 22:11:12 -08:00
Rhet Turnbull
6cb7dedd9b Updated debug info 2022-02-22 09:49:02 -08:00
Rhet Turnbull
39ba17dd1c Added debug output to exiftool 2022-02-22 06:40:25 -08:00
Rhet Turnbull
5b66962ac1 Fixed export of bursts with --uuid and --selected, #640 2022-02-21 22:58:54 -08:00
Rhet Turnbull
c05340f631 removed macos-12 as it doesn't work with Actions 2022-02-21 22:49:12 -08:00
Rhet Turnbull
f24c461cbb Added macos-12 2022-02-21 17:08:27 -08:00
Rhet Turnbull
c8ee679799 Added --sql command to exportdb 2022-02-21 16:06:16 -08:00
Rhet Turnbull
2966c9a60f Updated docs [skip ci] 2022-02-21 15:17:19 -08:00
Rhet Turnbull
acfcb0c49a Updated CHANGELOG.md [skip ci] 2022-02-21 11:39:04 -08:00
Rhet Turnbull
b92a681795 Added --ramdb option (#639) 2022-02-21 11:20:02 -08:00
Rhet Turnbull
1941e79d21 Updated CHANGELOG.md [skip ci] 2022-02-21 09:42:08 -08:00
Rhet Turnbull
5290fae2e0 Updated docs [skip ci] 2022-02-21 09:34:40 -08:00
Rhet Turnbull
ecbd370a47 Exportdb refactor (#638)
* Working on export_db refactor

* Added exportdb command, removed logic for missing export_db, #630

* Updated tests

* updated docs

* Added --config-only, #606

* Added validation for --exportdb

* Added --info to exportdb command

* Fixed exportdb --touch-file to migrate database if needed

* Added exportdb --migrate
2022-02-21 09:15:01 -08:00
Rhet Turnbull
d8204e65eb Allow multiple characters as path_sep, #634 2022-02-14 06:46:19 -08:00
Rhet Turnbull
9c26e5519b Added crash_reporter.py 2022-02-13 15:02:35 -08:00
Rhet Turnbull
060729c4c4 Added --debug and crash reporter to export, #628 2022-02-13 14:51:31 -08:00
Rhet Turnbull
65d51ab129 Updated docs [skip ci] 2022-02-13 00:21:27 -08:00
Rhet Turnbull
afbda030bc beta fix for #633, fix face regions in exiftool 2022-02-13 00:14:59 -08:00
Rhet Turnbull
d111d07fb7 Updated CHANGELOG.md [skip ci] 2022-02-12 21:13:56 -08:00
Rhet Turnbull
30abdddaf3 Added --force-update, #621 2022-02-12 21:01:16 -08:00
Rhet Turnbull
a2f329b8de Updated CHANGELOG.md [skip ci] 2022-02-12 17:59:48 -08:00
Rhet Turnbull
bfa888adc5 Added --force-update, #621 2022-02-12 17:49:40 -08:00
Rhet Turnbull
ac4083bfbb Fix for #630 2022-02-12 00:23:50 -08:00
Rhet Turnbull
5fb686ac0c Refactored fix for #627 2022-02-11 23:10:09 -08:00
Rhet Turnbull
49a7b80680 Fixed cleanup for #629 2022-02-11 06:18:17 -08:00
Rhet Turnbull
cb11967eac Implement #629, sqlite performance optimizatons for export db 2022-02-10 22:36:35 -08:00
Rhet Turnbull
a43bfc5a33 Updated CHANGELOG.md [skip ci] 2022-02-06 00:01:43 -08:00
Rhet Turnbull
1d6bc4e09e Additional fix for #615 2022-02-05 23:57:50 -08:00
Rhet Turnbull
3e14b718ef Updated docs [skip ci] 2022-02-05 23:12:42 -08:00
Rhet Turnbull
1ae6270561 Fixed exiftool to ignore unsupported file types, #615 2022-02-05 22:54:50 -08:00
Rhet Turnbull
55a601c07e Updated tests 2022-02-05 14:30:20 -08:00
Rhet Turnbull
7d67b81879 Updated CHANGELOG.md [skip ci] 2022-02-05 14:08:43 -08:00
Rhet Turnbull
cd02144ac3 Fix for --name searching only original_filename on Photos 5+, #594 2022-02-05 12:55:56 -08:00
Rhet Turnbull
9b247acd1c Fix for unicode in query strings, #618 2022-02-05 12:36:25 -08:00
Rhet Turnbull
942126ea3d Updated CHANGELOG.md [skip ci] 2022-02-05 10:56:18 -08:00
Rhet Turnbull
2b9ea11701 Updated docs [skip ci] 2022-02-05 10:39:35 -08:00
Rhet Turnbull
b3d3e14ffe Fix for #561, no really, I mean it this time 2022-02-05 10:36:23 -08:00
Rhet Turnbull
62ae5db9fd Updated CHANGELOG.md [skip ci] 2022-02-04 21:59:33 -08:00
Rhet Turnbull
77a49a09a1 Updated tests for #561 [skip ci] 2022-02-04 05:56:01 -08:00
Rhet Turnbull
06c5bbfcfd Updated docs [skip ci] 2022-02-03 22:49:56 -08:00
Rhet Turnbull
f3063d35be Fix for filenames with special characters, #561, #618 2022-02-03 22:46:11 -08:00
Rhet Turnbull
e32090bf39 Updated known issues [skip ci] 2022-02-01 06:53:25 -08:00
Rhet Turnbull
7ab500740b Added progress counter, #601 2022-01-29 19:02:25 -08:00
Rhet Turnbull
911bd30d28 Updated CHANGELOG.md [skip ci] 2022-01-29 19:02:00 -08:00
allcontributors[bot]
282857eae0 docs: add oPromessa as a contributor for ideas, test (#611)
* docs: update .all-contributorsrc [skip ci]

* docs: update README.md [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-29 14:08:36 -08:00
Rhet Turnbull
d8c2f99c06 Added --timestamp option for --verbose, #600 2022-01-29 11:59:41 -08:00
Rhet Turnbull
16d3f74366 Updated formatting for elapsed time, #604 2022-01-29 11:05:33 -08:00
Rhet Turnbull
5fc28139ea Updated docs [skip ci] 2022-01-29 10:55:41 -08:00
Rhet Turnbull
b7b6876688 Updated CHANGELOG.md [skip ci] 2022-01-29 10:03:31 -08:00
Rhet Turnbull
235dea329c Implemented #605, refactor out export2 2022-01-29 09:38:52 -08:00
Rhet Turnbull
5afdf6fc20 Fix for #564, --preview with --download-missing 2022-01-29 08:27:43 -08:00
Rhet Turnbull
385059e973 Updated CHANGELOG.md [skip ci] 2022-01-28 23:32:46 -08:00
Rhet Turnbull
62aed02070 Updated docs [skip ci] 2022-01-28 23:20:27 -08:00
Rhet Turnbull
6843b8661d Refactored photoexporter for performance, #591 2022-01-28 23:15:02 -08:00
Rhet Turnbull
9da747ea9d Refactoring to support #591 2022-01-27 21:37:12 -08:00
Rhet Turnbull
22964afc69 Performance improvements and refactoring, #462, partial for #591 2022-01-27 06:28:12 -08:00
Rhet Turnbull
3bc53fd92b Performance improvements, partial for #591 2022-01-25 20:37:58 -08:00
Rhet Turnbull
bd31120569 Version bump 2022-01-24 06:28:58 -08:00
Rhet Turnbull
6af124e4d3 Removed exportdb requirement from PhotoTemplate 2022-01-24 06:20:34 -08:00
Rhet Turnbull
b3b1d8f193 Updated CHANGELOG.md [skip ci] 2022-01-23 22:01:54 -08:00
Rhet Turnbull
785580115b Added query options to repl, #597 2022-01-23 21:57:51 -08:00
Rhet Turnbull
b4bd04c146 Added run command, #598 2022-01-23 18:38:16 -08:00
Rhet Turnbull
e88c6b8a59 Bug fix for get_photos_library_version 2022-01-23 18:06:19 -08:00
Rhet Turnbull
74868238f3 Performance improvements, added --profile 2022-01-23 17:14:55 -08:00
Xiaoliang Wu
61a300250d creat unit test for __all__ (#599) 2022-01-23 16:40:20 -08:00
Rhet Turnbull
d8dbc0866f Updated CHANGELOG.md [skip ci] 2022-01-22 14:43:11 -08:00
Rhet Turnbull
586d96ae74 Updated docs [skip ci] 2022-01-22 14:40:38 -08:00
Rhet Turnbull
81032a5745 Added tutorial.md, #596 2022-01-22 14:38:22 -08:00
Rhet Turnbull
c2d726beaf More refactoring of export code, #462 2022-01-22 10:44:29 -08:00
Rhet Turnbull
3bafdf7bfd Blackified files 2022-01-22 09:25:08 -08:00
Xiaoliang Wu
edcc7ea34f Create __all__ for all python files (#589)
* add __all__ to files "adjustmentsinfo.py" and "albuminfo.py"

* add __all__ to file "cli.py"

* add __all__ to all files that misses except files with prefix "_"
2022-01-22 09:22:47 -08:00
Rhet Turnbull
6261a7b5c9 More refactoring of export code, #462 2022-01-22 09:03:01 -08:00
Rhet Turnbull
881832c92d Removed warning from test 2022-01-18 08:08:58 -08:00
Xiaoliang Wu
47d4dc7ef0 Create __all__ for the file cli.py (#587)
* add __all__ to files "adjustmentsinfo.py" and "albuminfo.py"

* add __all__ to file "cli.py"
2022-01-17 22:03:48 -08:00
allcontributors[bot]
10ce81bf98 docs: add xwu64 as a contributor for code (#585)
* 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-15 22:49:56 -08:00
Xiaoliang Wu
98b3d9f81e add __all__ to files "adjustmentsinfo.py" and "albuminfo.py" (#584) 2022-01-15 22:49:03 -08:00
Rhet Turnbull
81cbb7dcc4 Refactored docstrings, #462 2022-01-15 17:45:38 -08:00
Rhet Turnbull
9517876bd0 Added ExportOptions to photoexporter.py, #462 2022-01-15 16:12:27 -08:00
Rhet Turnbull
231d132792 More refactoring of export code, #462 2022-01-14 21:57:27 -08:00
Rhet Turnbull
9ada5dfea4 More refactoring of export code, #462 2022-01-14 19:48:36 -08:00
Rhet Turnbull
476c94407f More refactoring of export code, #462 2022-01-14 18:31:50 -08:00
Rhet Turnbull
458da0e9b2 Refactored photoexporter sidecar writing, #462 2022-01-14 17:43:40 -08:00
Rhet Turnbull
66673012ac Updated tested versions 2022-01-14 17:10:28 -08:00
Rhet Turnbull
46f8b6dc5a Updated README.md 2022-01-14 15:05:15 -08:00
Rhet Turnbull
ee81e69ece Added dev tools 2022-01-14 15:02:33 -08:00
Rhet Turnbull
3927f05267 Added diff command 2022-01-09 09:35:42 -08:00
Rhet Turnbull
a010ab5a29 Added uuid command 2022-01-09 07:58:14 -08:00
Rhet Turnbull
c49bebd412 Updated CHANGELOG.md [skip ci] 2022-01-09 07:49:09 -08:00
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
753 changed files with 62350 additions and 10107 deletions

View File

@@ -250,6 +250,82 @@
"contributions": [
"bug"
]
},
{
"login": "oPromessa",
"name": "oPromessa",
"avatar_url": "https://avatars.githubusercontent.com/u/21261491?v=4",
"profile": "https://github.com/oPromessa",
"contributions": [
"bug",
"ideas",
"test"
]
},
{
"login": "spencerc99",
"name": "Spencer Chang",
"avatar_url": "https://avatars.githubusercontent.com/u/14796580?v=4",
"profile": "http://spencerchang.me",
"contributions": [
"bug"
]
},
{
"login": "dgleich",
"name": "David Gleich",
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
"profile": "https://www.cs.purdue.edu/homes/dgleich",
"contributions": [
"code"
]
},
{
"login": "alandefreitas",
"name": "Alan de Freitas",
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
"profile": "https://alandefreitas.github.io/alandefreitas/",
"contributions": [
"bug"
]
},
{
"login": "hyfen",
"name": "Andrew Louis",
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
"profile": "https://hyfen.net",
"contributions": [
"doc",
"code"
]
},
{
"login": "neebah",
"name": "neebah",
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
"profile": "https://github.com/neebah",
"contributions": [
"bug"
]
},
{
"login": "ahti123",
"name": "Ahti Liin",
"avatar_url": "https://avatars.githubusercontent.com/u/22232632?v=4",
"profile": "https://github.com/ahti123",
"contributions": [
"code",
"bug"
]
},
{
"login": "xwu64",
"name": "Xiaoliang Wu",
"avatar_url": "https://avatars.githubusercontent.com/u/10580396?v=4",
"profile": "https://github.com/xwu64",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

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

@@ -10,7 +10,7 @@ jobs:
max-parallel: 4
matrix:
os: [macos-10.15]
python-version: [3.7, 3.8, 3.9]
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v1
@@ -32,4 +32,4 @@ jobs:
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
python -m pytest tests/
python -m pytest -v tests/

2
.gitignore vendored
View File

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

View File

@@ -4,6 +4,501 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.47.7](https://github.com/RhetTbull/osxphotos/compare/v0.47.6...v0.47.7)
> 17 April 2022
- Theme [`#664`](https://github.com/RhetTbull/osxphotos/pull/664)
- Updated docs [skip ci] [`b09323b`](https://github.com/RhetTbull/osxphotos/commit/b09323b9fbce53f6ea7ace949952772f81d79b70)
- Fixed typing in examples [`1f40161`](https://github.com/RhetTbull/osxphotos/commit/1f401619500fb56cf0bfc6b02b1af1a09a2dabd8)
- Version bump [`213d84e`](https://github.com/RhetTbull/osxphotos/commit/213d84e9648efcb5c37c48aa45e3d27d7cb76d80)
- Quoted path in repl [`d1aa4e9`](https://github.com/RhetTbull/osxphotos/commit/d1aa4e92bdc62948186dbee2cab40a644beb8112)
- Added cov.xml [skip ci] [`9c0b910`](https://github.com/RhetTbull/osxphotos/commit/9c0b910046e2b5702d09d42c89556ed970191f80)
#### [v0.47.6](https://github.com/RhetTbull/osxphotos/compare/v0.47.5...v0.47.6)
> 27 March 2022
- fix verbose output when redirected to file, #661 [`382d097`](https://github.com/RhetTbull/osxphotos/commit/382d097285519e274bdb0cd16d6805152aa4a918)
- Updated docs [skip ci] [`6875427`](https://github.com/RhetTbull/osxphotos/commit/68754273de33ac9ed767fd43edcfe8b9c92e3a56)
- version bump [`d28a2fe`](https://github.com/RhetTbull/osxphotos/commit/d28a2fe9bb69034c19446c3dc70555b6043a43a0)
#### [v0.47.5](https://github.com/RhetTbull/osxphotos/compare/v0.47.4...v0.47.5)
> 12 March 2022
- Richify [`#653`](https://github.com/RhetTbull/osxphotos/pull/653)
- Added --watch, --breakpoint [`#652`](https://github.com/RhetTbull/osxphotos/pull/652)
- Hack to fix #654 when utime fails on NAS [`#654`](https://github.com/RhetTbull/osxphotos/issues/654)
- Debug updates [`de1900f`](https://github.com/RhetTbull/osxphotos/commit/de1900f10aaac8d703ef5d850a64f18dd5e01d40)
- Updated docs [skip ci] [`fccd746`](https://github.com/RhetTbull/osxphotos/commit/fccd746c581a319a8d1d5063133cbb9d5a4e1778)
- Changed return val of _should_update_photo to enum for easier debugging [`bbcc3ac`](https://github.com/RhetTbull/osxphotos/commit/bbcc3acba9a4dd3a9ebcc8136782005d35e46255)
- Updated crash_reporter to include crash data [`1227465`](https://github.com/RhetTbull/osxphotos/commit/1227465aa7e1d5a2fdaba4fc45cf917728d31170)
- Fixed missing pdb.py issue for pyinstaller, partial for #659 [`e272e95`](https://github.com/RhetTbull/osxphotos/commit/e272e95a856e5b448eb6ac85818757b974dfd6d4)
#### [v0.47.4](https://github.com/RhetTbull/osxphotos/compare/v0.47.3...v0.47.4)
> 2 March 2022
- Added --tmpdir, #650 [`#651`](https://github.com/RhetTbull/osxphotos/pull/651)
- Version bump [`6b342a1`](https://github.com/RhetTbull/osxphotos/commit/6b342a1733fa66d1663de2e3a234970f427a679f)
- Version bump [`f132e9a`](https://github.com/RhetTbull/osxphotos/commit/f132e9a8438023e4c69c7fb767d24dea7465db4d)
#### [v0.47.3](https://github.com/RhetTbull/osxphotos/compare/v0.47.2...v0.47.3)
> 27 February 2022
- Help topic [`#644`](https://github.com/RhetTbull/osxphotos/pull/644)
- updated docs [skip ci] [`ce73c9c`](https://github.com/RhetTbull/osxphotos/commit/ce73c9cab81fdd223dd49f2ff38608d553198412)
- Added -v to pytest [`8be6a98`](https://github.com/RhetTbull/osxphotos/commit/8be6a98c3208b5da0fa620d1884a17275ab56599)
#### [v0.47.2](https://github.com/RhetTbull/osxphotos/compare/v0.47.1...v0.47.2)
> 27 February 2022
- Updated README.md [`b275280`](https://github.com/RhetTbull/osxphotos/commit/b275280a1f3e1b1a61dcc95aefebd4e326a47377)
- Updated docs [skip ci] [`c95f682`](https://github.com/RhetTbull/osxphotos/commit/c95f682ca647f9b9e0718da658bdf7c735571f84)
- Fix for --load-config, #643 [`feb9538`](https://github.com/RhetTbull/osxphotos/commit/feb9538d1c5ad6569232e5900393befb8ec1a57e)
#### [v0.47.1](https://github.com/RhetTbull/osxphotos/compare/v0.47.0...v0.47.1)
> 26 February 2022
- Fixed entry point [`d275367`](https://github.com/RhetTbull/osxphotos/commit/d2753672f36a0c84b7be143a47cc85cd6c99cb6d)
#### [v0.47.0](https://github.com/RhetTbull/osxphotos/compare/v0.46.6...v0.47.0)
> 26 February 2022
- CLI refactor [`#642`](https://github.com/RhetTbull/osxphotos/pull/642)
- Updated docs [skip ci] [`6fae979`](https://github.com/RhetTbull/osxphotos/commit/6fae97906124c9284e382170e20c8ab9999105b0)
- Fixed 3.10 in yaml [`3704fc4`](https://github.com/RhetTbull/osxphotos/commit/3704fc4a23e83ff2d16d6d221fb6c752dabcedca)
- Dropped 3.7 [`7883fc1`](https://github.com/RhetTbull/osxphotos/commit/7883fc1911057df9a4c596375b498e85a73c1bec)
#### [v0.46.6](https://github.com/RhetTbull/osxphotos/compare/v0.46.5...v0.46.6)
> 26 February 2022
- Updated tests [`43e1cb1`](https://github.com/RhetTbull/osxphotos/commit/43e1cb18cc65b1abe1f49b464563e816b2ed1cff)
- Updated docs [skip ci] [`3a990e3`](https://github.com/RhetTbull/osxphotos/commit/3a990e39971d838e52d5f19bf28b8253c4c7b811)
- Bug fix for bitmath types in saved config [`26f916e`](https://github.com/RhetTbull/osxphotos/commit/26f916e4cbf4f28154c47aa2de1fdbc0aebc65b3)
#### [v0.46.5](https://github.com/RhetTbull/osxphotos/compare/v0.46.4...v0.46.5)
> 24 February 2022
- Updated tested versions [`4d1b1db`](https://github.com/RhetTbull/osxphotos/commit/4d1b1db2a7cf34afaa2dc5dbebc69021ff77964f)
#### [v0.46.4](https://github.com/RhetTbull/osxphotos/compare/v0.46.1...v0.46.4)
> 24 February 2022
- Removed debug code from exiftool, fixed #641 [`#641`](https://github.com/RhetTbull/osxphotos/issues/641)
- Added debug output to exiftool [`39ba17d`](https://github.com/RhetTbull/osxphotos/commit/39ba17dd1cb4d8a61ab4dc8d5cff12ff9871eee0)
- Fixed export of bursts with --uuid and --selected, #640 [`5b66962`](https://github.com/RhetTbull/osxphotos/commit/5b66962ac1bc1f48106fb8eeb600e6010088dc3b)
- Added --sql command to exportdb [`c8ee679`](https://github.com/RhetTbull/osxphotos/commit/c8ee6797999af954c32e96ac3799a19002f4f0fe)
- Updated docs [skip ci] [`2966c9a`](https://github.com/RhetTbull/osxphotos/commit/2966c9a60fc828afdf34263b759159a3ade31897)
- Updated debug info [`6cb7ded`](https://github.com/RhetTbull/osxphotos/commit/6cb7dedd9be53d2c62489125fc44b9f4dccfb7ae)
#### [v0.46.1](https://github.com/RhetTbull/osxphotos/compare/v0.46.0...v0.46.1)
> 21 February 2022
- Added --ramdb option [`#639`](https://github.com/RhetTbull/osxphotos/pull/639)
#### [v0.46.0](https://github.com/RhetTbull/osxphotos/compare/v0.45.12...v0.46.0)
> 21 February 2022
- Exportdb refactor [`#638`](https://github.com/RhetTbull/osxphotos/pull/638)
- Updated docs [skip ci] [`5290fae`](https://github.com/RhetTbull/osxphotos/commit/5290fae2e0ad062750348aedfee4feaf7b2e769f)
#### [v0.45.12](https://github.com/RhetTbull/osxphotos/compare/v0.45.11...v0.45.12)
> 14 February 2022
- Allow multiple characters as path_sep, #634 [`d8204e6`](https://github.com/RhetTbull/osxphotos/commit/d8204e65eb740cece468ef021cbdf45d896d954e)
- Added --debug and crash reporter to export, #628 [`060729c`](https://github.com/RhetTbull/osxphotos/commit/060729c4c4255651c6ee8149989d9de541d0a6aa)
- Added crash_reporter.py [`9c26e55`](https://github.com/RhetTbull/osxphotos/commit/9c26e5519b2d48f3a0ae80d1cc4a765c12b62d40)
#### [v0.45.11](https://github.com/RhetTbull/osxphotos/compare/v0.45.10...v0.45.11)
> 13 February 2022
- beta fix for #633, fix face regions in exiftool [`afbda03`](https://github.com/RhetTbull/osxphotos/commit/afbda030bce87f914445ebbced3f0e110e2e203b)
- Updated docs [skip ci] [`65d51ab`](https://github.com/RhetTbull/osxphotos/commit/65d51ab1290e7c7804021e24829b93f5dce81245)
#### [v0.45.10](https://github.com/RhetTbull/osxphotos/compare/v0.45.9...v0.45.10)
> 12 February 2022
- Added --force-update, #621 [`30abddd`](https://github.com/RhetTbull/osxphotos/commit/30abdddaf3765f1d604984d4781b78b7806871e1)
#### [v0.45.9](https://github.com/RhetTbull/osxphotos/compare/v0.45.8...v0.45.9)
> 12 February 2022
- Added --force-update, #621 [`bfa888a`](https://github.com/RhetTbull/osxphotos/commit/bfa888adc5658a2845dcaa9b7ea360926ed4f000)
- Refactored fix for #627 [`5fb686a`](https://github.com/RhetTbull/osxphotos/commit/5fb686ac0c231932c2695fc550a0824307bd3c5f)
- Fix for #630 [`ac4083b`](https://github.com/RhetTbull/osxphotos/commit/ac4083bfbbabc8550718f0f7f8aadc635c05eb25)
#### [v0.45.8](https://github.com/RhetTbull/osxphotos/compare/v0.45.6...v0.45.8)
> 5 February 2022
- Fixed exiftool to ignore unsupported file types, #615 [`1ae6270`](https://github.com/RhetTbull/osxphotos/commit/1ae627056113fc4655f1b24cfbbdf0efc04489e7)
- Updated tests [`55a601c`](https://github.com/RhetTbull/osxphotos/commit/55a601c07ea1384623c55d5c1d26b568df5d7823)
- Additional fix for #615 [`1d6bc4e`](https://github.com/RhetTbull/osxphotos/commit/1d6bc4e09e3c2359a21f842fadd781920606812e)
#### [v0.45.6](https://github.com/RhetTbull/osxphotos/compare/v0.45.5...v0.45.6)
> 5 February 2022
- Fix for unicode in query strings, #618 [`9b247ac`](https://github.com/RhetTbull/osxphotos/commit/9b247acd1cc4b2def59fdd18a6fb3c8eb9914f11)
- Fix for --name searching only original_filename on Photos 5+, #594 [`cd02144`](https://github.com/RhetTbull/osxphotos/commit/cd02144ac33cc1c13a20358133971c84d35b8a57)
#### [v0.45.5](https://github.com/RhetTbull/osxphotos/compare/v0.45.4...v0.45.5)
> 5 February 2022
- Fix for #561, no really, I mean it this time [`b3d3e14`](https://github.com/RhetTbull/osxphotos/commit/b3d3e14ffe41fbb22edb614b24f3985f379766a2)
- Updated docs [skip ci] [`2b9ea11`](https://github.com/RhetTbull/osxphotos/commit/2b9ea11701799af9a661a8e2af70fca97235f487)
- Updated tests for #561 [skip ci] [`77a49a0`](https://github.com/RhetTbull/osxphotos/commit/77a49a09a1bee74113a7114c543fbc25fa410ffc)
#### [v0.45.4](https://github.com/RhetTbull/osxphotos/compare/v0.45.3...v0.45.4)
> 3 February 2022
- docs: add oPromessa as a contributor for ideas, test [`#611`](https://github.com/RhetTbull/osxphotos/pull/611)
- Fix for filenames with special characters, #561, #618 [`f3063d3`](https://github.com/RhetTbull/osxphotos/commit/f3063d35be3c96342d83dbd87ddd614a2001bff4)
- Updated docs [skip ci] [`06c5bbf`](https://github.com/RhetTbull/osxphotos/commit/06c5bbfcfdf591a4a5d43f1456adaa27385fe01a)
- Added progress counter, #601 [`7ab5007`](https://github.com/RhetTbull/osxphotos/commit/7ab500740b28594dcd778140e10991f839220e9d)
- Updated known issues [skip ci] [`e32090b`](https://github.com/RhetTbull/osxphotos/commit/e32090bf39cb786171b49443f878ffdbab774420)
#### [v0.45.3](https://github.com/RhetTbull/osxphotos/compare/v0.45.2...v0.45.3)
> 29 January 2022
- Added --timestamp option for --verbose, #600 [`d8c2f99`](https://github.com/RhetTbull/osxphotos/commit/d8c2f99c06bc6f72bf2cb1a13c5765824fe3cbba)
- Updated docs [skip ci] [`5fc2813`](https://github.com/RhetTbull/osxphotos/commit/5fc28139ea0374bc3e228c0432b8a41ada430389)
- Updated formatting for elapsed time, #604 [`16d3f74`](https://github.com/RhetTbull/osxphotos/commit/16d3f743664396d43b3b3028a5e7a919ec56d9e1)
#### [v0.45.2](https://github.com/RhetTbull/osxphotos/compare/v0.45.0...v0.45.2)
> 29 January 2022
- Implemented #605, refactor out export2 [`235dea3`](https://github.com/RhetTbull/osxphotos/commit/235dea329c98ab8fa61565c09a1b4a83e5d99043)
- Fix for #564, --preview with --download-missing [`5afdf6f`](https://github.com/RhetTbull/osxphotos/commit/5afdf6fc20a3cb6eb2b0217d8b3be20295eb7ba4)
#### [v0.45.0](https://github.com/RhetTbull/osxphotos/compare/v0.44.13...v0.45.0)
> 28 January 2022
- Performance improvements and refactoring, #462, partial for #591 [`22964af`](https://github.com/RhetTbull/osxphotos/commit/22964afc6988166218413125d7a62348bb858a83)
- Refactored photoexporter for performance, #591 [`6843b86`](https://github.com/RhetTbull/osxphotos/commit/6843b8661d41d42368794c77304fc07194e7af18)
- Performance improvements, partial for #591 [`3bc53fd`](https://github.com/RhetTbull/osxphotos/commit/3bc53fd92b3222c6959e7aa12310811db41b83fe)
#### [v0.44.13](https://github.com/RhetTbull/osxphotos/compare/v0.44.12...v0.44.13)
> 24 January 2022
- Removed exportdb requirement from PhotoTemplate [`6af124e`](https://github.com/RhetTbull/osxphotos/commit/6af124e4d3a0e26c48f435452920020cd42afa1c)
- Version bump [`bd31120`](https://github.com/RhetTbull/osxphotos/commit/bd3112056920806f565be2c0c12caf4f2aff5231)
#### [v0.44.12](https://github.com/RhetTbull/osxphotos/compare/v0.44.11...v0.44.12)
> 23 January 2022
- Added query options to repl, #597 [`7855801`](https://github.com/RhetTbull/osxphotos/commit/785580115b29f5ccb895de22be1243f56dbb43dc)
- Added run command, #598 [`b4bd04c`](https://github.com/RhetTbull/osxphotos/commit/b4bd04c1461d0b427937f541403305bc979bcf4f)
- Bug fix for get_photos_library_version [`e88c6b8`](https://github.com/RhetTbull/osxphotos/commit/e88c6b8a59dfd947f6cf3c7eac9c92519ab781a3)
#### [v0.44.11](https://github.com/RhetTbull/osxphotos/compare/v0.44.10...v0.44.11)
> 23 January 2022
- creat unit test for __all__ [`#599`](https://github.com/RhetTbull/osxphotos/pull/599)
- Performance improvements, added --profile [`7486823`](https://github.com/RhetTbull/osxphotos/commit/74868238f3b1ee18feb744f137f5c14ef8e36ffc)
#### [v0.44.10](https://github.com/RhetTbull/osxphotos/compare/v0.44.9...v0.44.10)
> 22 January 2022
- Create __all__ for all python files [`#589`](https://github.com/RhetTbull/osxphotos/pull/589)
- Create __all__ for the file cli.py [`#587`](https://github.com/RhetTbull/osxphotos/pull/587)
- docs: add xwu64 as a contributor for code [`#585`](https://github.com/RhetTbull/osxphotos/pull/585)
- add __all__ to files "adjustmentsinfo.py" and "albuminfo.py" [`#584`](https://github.com/RhetTbull/osxphotos/pull/584)
- More refactoring of export code, #462 [`6261a7b`](https://github.com/RhetTbull/osxphotos/commit/6261a7b5c96ac43aece66b72b9e27a90854accfa)
- Added ExportOptions to photoexporter.py, #462 [`9517876`](https://github.com/RhetTbull/osxphotos/commit/9517876bd06572238648a6362a309063b86007e7)
- Blackified files [`3bafdf7`](https://github.com/RhetTbull/osxphotos/commit/3bafdf7bfd5f7992b2e0c12496c55e7be1f57455)
- More refactoring of export code, #462 [`c2d726b`](https://github.com/RhetTbull/osxphotos/commit/c2d726beafabe76cf4d5fb3213447c900129b8c0)
- Refactored photoexporter sidecar writing, #462 [`458da0e`](https://github.com/RhetTbull/osxphotos/commit/458da0e9b2b82a78cec30191c5bf1ee2ed993acf)
#### [v0.44.9](https://github.com/RhetTbull/osxphotos/compare/v0.44.8...v0.44.9)
> 9 January 2022
- Added diff command [`3927f05`](https://github.com/RhetTbull/osxphotos/commit/3927f052670b2a1c31cced1f8278a0ffe519a3eb)
- Added uuid command [`a010ab5`](https://github.com/RhetTbull/osxphotos/commit/a010ab5a299470782b938e689a7ddc336513065e)
#### [v0.44.8](https://github.com/RhetTbull/osxphotos/compare/v0.44.7...v0.44.8)
> 9 January 2022
- docs: add ahti123 as a contributor for code, bug [`#578`](https://github.com/RhetTbull/osxphotos/pull/578)
- changing photos_5 version constant to satisfy version 5001 [`#577`](https://github.com/RhetTbull/osxphotos/pull/577)
- Added grep command to CLI [`4dd838b`](https://github.com/RhetTbull/osxphotos/commit/4dd838b8bcb639eba3df9cb60a7cd28f45b22833)
- Added test for #576 [`92fced7`](https://github.com/RhetTbull/osxphotos/commit/92fced75da38f1c47be8d3d9d4ee22463ad029b9)
- Added sqlgrep [`53c701c`](https://github.com/RhetTbull/osxphotos/commit/53c701cc0ebd38db255c1ce694391b38dbb5fe01)
- Fix for #575, database version 5001 [`5a8105f`](https://github.com/RhetTbull/osxphotos/commit/5a8105f5a02080368ad22717c064afcb0748f646)
- Updated docs [skip ci] [`64a0760`](https://github.com/RhetTbull/osxphotos/commit/64a0760a47205a452e015a860f39f45bba67164a)
#### [v0.44.7](https://github.com/RhetTbull/osxphotos/compare/v0.44.6...v0.44.7)
> 8 January 2022
- Fix for #576, error exporting edited live photos [`2e7db47`](https://github.com/RhetTbull/osxphotos/commit/2e7db47806683fdd0db4d1d75e42471d2f127d4d)
#### [v0.44.6](https://github.com/RhetTbull/osxphotos/compare/v0.44.5...v0.44.6)
> 6 January 2022
- Fix for burst images with pick type = 0, partial fix for #571 [`d2d56a7`](https://github.com/RhetTbull/osxphotos/commit/d2d56a7f7118aeffa7ac81cc474fdd4fb4843065)
#### [v0.44.5](https://github.com/RhetTbull/osxphotos/compare/v0.44.4...v0.44.5)
> 6 January 2022
- More refactoring of export code, #462 [`0c9bd87`](https://github.com/RhetTbull/osxphotos/commit/0c9bd8760261770e11b0fa59153f49f2d65e2c2f)
- Fix for #570 [`661a573`](https://github.com/RhetTbull/osxphotos/commit/661a573bf50353fb2393c604080ffe0790ade59c)
- version bump [skip ci] [`b4897ff`](https://github.com/RhetTbull/osxphotos/commit/b4897ff1b5d2bc00f34158345b2b5fe85f1490ac)
#### [v0.44.4](https://github.com/RhetTbull/osxphotos/compare/v0.44.3...v0.44.4)
> 4 January 2022
- Refactored photoinfo, photoexporter; #462 [`a73dc72`](https://github.com/RhetTbull/osxphotos/commit/a73dc72558b77152f4c90f143b6a60924b8905c8)
- More refactoring of export code, #462 [`147b30f`](https://github.com/RhetTbull/osxphotos/commit/147b30f97308db65868dc7a8d177d77ad0d0ad40)
- Export DB can now reside outside export directory, #568 [`76aee7f`](https://github.com/RhetTbull/osxphotos/commit/76aee7f189b4b32e2e263a4e798711713ed17a14)
#### [v0.44.3](https://github.com/RhetTbull/osxphotos/compare/v0.44.2...v0.44.3)
> 31 December 2021
- ImageConverter now uses generic context; #562 [`a3b2784`](https://github.com/RhetTbull/osxphotos/commit/a3b2784f3177a753b78965b8ca205ca9bbb08168)
- Updated tests and docs [`1391675`](https://github.com/RhetTbull/osxphotos/commit/1391675a3a45be0d6800a68c8bcc6d0d55d1ab7a)
- Updated docs [skip ci] [`cbe79ee`](https://github.com/RhetTbull/osxphotos/commit/cbe79ee98cae68e0789df275220f5a5870a8bd91)
#### [v0.44.2](https://github.com/RhetTbull/osxphotos/compare/v0.44.1...v0.44.2)
> 31 December 2021
- Bug fix for #559 [`42426b9`](https://github.com/RhetTbull/osxphotos/commit/42426b95ee786b2d53482d3d931a0b962a4db20d)
#### [v0.44.1](https://github.com/RhetTbull/osxphotos/compare/v0.44.0...v0.44.1)
> 31 December 2021
- Added --skip-uuid, --skip-uuid-from-file, #563 [`04930c3`](https://github.com/RhetTbull/osxphotos/commit/04930c3644da99c1923c4e3aaa9213902aeadfd1)
#### [v0.44.0](https://github.com/RhetTbull/osxphotos/compare/v0.43.9...v0.44.0)
> 31 December 2021
- Added support for projects, implements #559 [`44594a8`](https://github.com/RhetTbull/osxphotos/commit/44594a8e437c20bae6fd8eecb74075d49da4b91f)
- Updated docs [skip ci] [`c4e3c5a`](https://github.com/RhetTbull/osxphotos/commit/c4e3c5a8beac1db00533f7820ab8249cf351aef0)
- Fixed test for #561 [`690d981`](https://github.com/RhetTbull/osxphotos/commit/690d981f310b083f5f58407cc879bca494730765)
#### [v0.43.9](https://github.com/RhetTbull/osxphotos/compare/v0.43.8...v0.43.9)
> 28 December 2021
- Fix for accented characters in album names, #561 [`03f4e7c`](https://github.com/RhetTbull/osxphotos/commit/03f4e7cc3473c276dfd7c7e6ad64e4dfe5b32011)
#### [v0.43.8](https://github.com/RhetTbull/osxphotos/compare/v0.43.7...v0.43.8)
> 26 December 2021
- Fixed #463 [`#463`](https://github.com/RhetTbull/osxphotos/issues/463)
- Updated docs [skip ci] [`181f678`](https://github.com/RhetTbull/osxphotos/commit/181f678d9eda8bc8acca11b4ebd470900f30bdcb)
- Added install/uninstall commands, #531 [`085f482`](https://github.com/RhetTbull/osxphotos/commit/085f482820af2d51f0d411c7e8a7a27329bf0722)
- Implement #323 [`debb17c`](https://github.com/RhetTbull/osxphotos/commit/debb17c9520bec25d725426feaa512745e9d4ec0)
- Updated docs [skip ci] [`0e54a08`](https://github.com/RhetTbull/osxphotos/commit/0e54a08ae07853c4cdb2c548bdba27335cfc32ba)
- Added get_photos_library_version [`b71c752`](https://github.com/RhetTbull/osxphotos/commit/b71c752e9d2c59412baf812bfc50e6358ea3f02e)
#### [v0.43.7](https://github.com/RhetTbull/osxphotos/compare/v0.43.6...v0.43.7)
> 21 December 2021
- Adds missing f-string to retry message [`#553`](https://github.com/RhetTbull/osxphotos/pull/553)
- Update issue templates [`e7bd80e`](https://github.com/RhetTbull/osxphotos/commit/e7bd80e05f94238fd41e478e32c1709b442eb361)
- Partial fix for #556 [`a08a653`](https://github.com/RhetTbull/osxphotos/commit/a08a653f202a49853780ab4a686bf3dfbc32a491)
- Updated all-contributors [`e1f1772`](https://github.com/RhetTbull/osxphotos/commit/e1f1772080d24373ceb5791683615451cd390874)
- Version bump [`6ce1b83`](https://github.com/RhetTbull/osxphotos/commit/6ce1b83ca2c7f0c6f9c86757602b81df1d9bf453)
#### [v0.43.6](https://github.com/RhetTbull/osxphotos/compare/v0.43.5...v0.43.6)
> 10 December 2021
- Fixes typo in README [`#548`](https://github.com/RhetTbull/osxphotos/pull/548)
- docs: add alandefreitas as a contributor for bug [`#551`](https://github.com/RhetTbull/osxphotos/pull/551)
- docs: add dgleich as a contributor for code [`#541`](https://github.com/RhetTbull/osxphotos/pull/541)
- Updated docs [`197e566`](https://github.com/RhetTbull/osxphotos/commit/197e5663df058a013ce2d6f8c5fd7ff71a5cc46e)
- Added test library for Monterey on M1 [`3e038bf`](https://github.com/RhetTbull/osxphotos/commit/3e038bf124b98d6b74f19dd4db0f8f1e3c48e787)
- Updated docs [skip ci] [`f6dedaa`](https://github.com/RhetTbull/osxphotos/commit/f6dedaa6197dc244616c5b4e9e8ce42ce6b7a252)
- Added MomentInfo for Photos 5+, #71 [`a52b4d2`](https://github.com/RhetTbull/osxphotos/commit/a52b4d2f43970086bf25659bd58dc8479b841704)
- Fixed error for missing photo path, #547 [`0906dbe`](https://github.com/RhetTbull/osxphotos/commit/0906dbe6370922b4c9649350014ed8a21d29c4fd)
#### [v0.43.5](https://github.com/RhetTbull/osxphotos/compare/v0.43.4...v0.43.5)
> 25 November 2021
- Updated dependencies for pyobjc 8.0 [`7d92359`](https://github.com/RhetTbull/osxphotos/commit/7d923590ae4df941b1b9d35c21937c03eb7b4284)
#### [v0.43.4](https://github.com/RhetTbull/osxphotos/compare/v0.43.3...v0.43.4)
> 11 November 2021
- Fix for --use-photokit with --skip-live, #537 [`0e6c92d`](https://github.com/RhetTbull/osxphotos/commit/0e6c92dbd951dd0e63cfb8b6d64e6ab96ece5955)
#### [v0.43.3](https://github.com/RhetTbull/osxphotos/compare/v0.43.1...v0.43.3)
> 7 November 2021
- Updated docs [skip ci] [`fb583e2`](https://github.com/RhetTbull/osxphotos/commit/fb583e28e0fc2c23bf24052db8a5ee669d8c92f5)
- Updated OTL to MTL [`2ffcf1e`](https://github.com/RhetTbull/osxphotos/commit/2ffcf1e82bfc013a4a9e0e7a709a7c1395c074ce)
- Test fixes for Monterey/M1 [`51ba549`](https://github.com/RhetTbull/osxphotos/commit/51ba54971a874cfce00368aa5be5380b3439c254)
#### [v0.43.1](https://github.com/RhetTbull/osxphotos/compare/v0.43.0...v0.43.1)
> 30 October 2021
- Dependency update for Monterey [`818f4f4`](https://github.com/RhetTbull/osxphotos/commit/818f4f45a4ce520b0ba1c688eabd2f4311be9540)
- Updated docs [skip ci] [`2cf19f6`](https://github.com/RhetTbull/osxphotos/commit/2cf19f6af1a03767e4d53eee556c4d3ed9af1776)
#### [v0.43.0](https://github.com/RhetTbull/osxphotos/compare/v0.42.94...v0.43.0)
> 28 October 2021
- Updated for Monterey 12.0.1 release [`ef82c6e`](https://github.com/RhetTbull/osxphotos/commit/ef82c6e32b536b0677530133892f95b852c6dce0)
#### [v0.42.94](https://github.com/RhetTbull/osxphotos/compare/v0.42.93...v0.42.94)
> 15 October 2021
- docs: add spencerc99 as a contributor for bug [`#527`](https://github.com/RhetTbull/osxphotos/pull/527)
- Fix for #526 with --update [`419b34e`](https://github.com/RhetTbull/osxphotos/commit/419b34ea73f15ccbe29f51896e11e9735ea5786b)
- Updated docs [skip ci] [`0e9b9d6`](https://github.com/RhetTbull/osxphotos/commit/0e9b9d625190b94c1dd68276e3b0e5367002d87c)
- Fixed FileUtil to use correct import [`f64c4ed`](https://github.com/RhetTbull/osxphotos/commit/f64c4ed374c120a95fe8adea26bd44852ca67e31)
#### [v0.42.93](https://github.com/RhetTbull/osxphotos/compare/v0.42.92...v0.42.93)
> 11 October 2021
- Fix for #526 [`202bc11`](https://github.com/RhetTbull/osxphotos/commit/202bc1144bc842ddec825eef0745830d56170aba)
- Updated README.md [skip ci] [`a0c654e`](https://github.com/RhetTbull/osxphotos/commit/a0c654e43f4aa5389a96c3c84fd7037c33d23404)
#### [v0.42.92](https://github.com/RhetTbull/osxphotos/compare/v0.42.91...v0.42.92)
> 11 October 2021
- docs: add oPromessa as a contributor for bug [`#525`](https://github.com/RhetTbull/osxphotos/pull/525)
- Fix for #524 [`04ac0a1`](https://github.com/RhetTbull/osxphotos/commit/04ac0a11215b275178013e60c6a61b9f1b3603c9)
- Fix for #525 [`d2b0bd4`](https://github.com/RhetTbull/osxphotos/commit/d2b0bd4e28cfdf3c930aa6ae3317549327b0e29c)
- Updated docs [skip ci] [`2bb677d`](https://github.com/RhetTbull/osxphotos/commit/2bb677dc19abaf254bc66e2cd788676e0613e548)
#### [v0.42.91](https://github.com/RhetTbull/osxphotos/compare/v0.42.90...v0.42.91)
> 11 October 2021
- Updated docs [skip ci] [`b23e74f`](https://github.com/RhetTbull/osxphotos/commit/b23e74f8f5a8387564108c330c3f8ac11189860d)
- Added python 3.10 to supported versions [`3f81a3c`](https://github.com/RhetTbull/osxphotos/commit/3f81a3c179dde37e9811ef19c847920bb3bd514c)
- Updated dependencies [`a895833`](https://github.com/RhetTbull/osxphotos/commit/a895833c7f0a264488e671f1735f9e10d2618e2d)
#### [v0.42.90](https://github.com/RhetTbull/osxphotos/compare/v0.42.89...v0.42.90)
> 30 September 2021
- Updated REPL, now with more cowbell [`c472698`](https://github.com/RhetTbull/osxphotos/commit/c472698b1d0d8ff9f4d1bde715859bf766f99290)
- Updated docs [skip ci] [`1ddb1de`](https://github.com/RhetTbull/osxphotos/commit/1ddb1de99841e65b690ffc1cbcc5e42e6e25f727)
#### [v0.42.89](https://github.com/RhetTbull/osxphotos/compare/v0.42.88...v0.42.89)
> 26 September 2021
- Updated docs [skip ci] [`bfbc156`](https://github.com/RhetTbull/osxphotos/commit/bfbc156821d2d262b7bd9c4437e23e310da10769)
- Updated docs [skip ci] [`3abaa5a`](https://github.com/RhetTbull/osxphotos/commit/3abaa5ae84ca44cd900f1e3af4532ab405d41a09)
- Fixed AlbumInfo.owner, #239 [`bfd6274`](https://github.com/RhetTbull/osxphotos/commit/bfd627460255c65f870bca6d036401e8792d29d5)
#### [v0.42.88](https://github.com/RhetTbull/osxphotos/compare/v0.42.87...v0.42.88)
> 26 September 2021
- Performance fix for #239, owner [`14710e3`](https://github.com/RhetTbull/osxphotos/commit/14710e31789d71b2c948a37722fb6054aca4d85e)
- version bump [`06138e1`](https://github.com/RhetTbull/osxphotos/commit/06138e15d0b87e4865a9ef0cc542303edb44c861)
#### [v0.42.87](https://github.com/RhetTbull/osxphotos/compare/v0.42.86...v0.42.87)
> 26 September 2021
#### [v0.42.86](https://github.com/RhetTbull/osxphotos/compare/v0.42.85...v0.42.86)
> 26 September 2021
- Fix for #517, #239 [`ac47df8`](https://github.com/RhetTbull/osxphotos/commit/ac47df8475762fe8c8f63ad5ffa83b1e20d116b8)
- Fixed formatting [`6adafb8`](https://github.com/RhetTbull/osxphotos/commit/6adafb8ce70e95a9f0bec1a3db6362742fcd1b0d)
- Updated docs [skip ci] [`725f7c8`](https://github.com/RhetTbull/osxphotos/commit/725f7c87351353efeee8c43c3c7f8a95acb14490)
#### [v0.42.85](https://github.com/RhetTbull/osxphotos/compare/v0.42.84...v0.42.85)
> 25 September 2021
- Implemented PhotoInfo.owner, AlbumInfo.owner, #216, #239 [`c4b7c26`](https://github.com/RhetTbull/osxphotos/commit/c4b7c2623f077d9964d5d578ce6c01bb83fab088)
- Updated docs [skip ci] [`59ba325`](https://github.com/RhetTbull/osxphotos/commit/59ba325273b2f16935be944fd46c1237ce637bb8)
#### [v0.42.84](https://github.com/RhetTbull/osxphotos/compare/v0.42.83...v0.42.84)
> 25 September 2021
- Fix for #516 [`e3e1da2`](https://github.com/RhetTbull/osxphotos/commit/e3e1da2fd898896595fc851288f905bd4e2150f8)
- Updated docs [skip ci] [`64c226b`](https://github.com/RhetTbull/osxphotos/commit/64c226b85529581e393a2d0604b41c37a8dc2eaf)
- Update docs [`c429a86`](https://github.com/RhetTbull/osxphotos/commit/c429a860b1ebeb77f3c3e36e9660fc9153d85d11)
#### [v0.42.83](https://github.com/RhetTbull/osxphotos/compare/v0.42.82...v0.42.83)
> 15 September 2021
- Fixed detected_text to use image orientation if available [`dd08c7f`](https://github.com/RhetTbull/osxphotos/commit/dd08c7f701335a7e1e30fda251e6ad20ff781652)
- Added twine [`16335a6`](https://github.com/RhetTbull/osxphotos/commit/16335a6bd66eaa53fd1c390901e2fb028059d8e1)
- Added wheel [`e0f6d8e`](https://github.com/RhetTbull/osxphotos/commit/e0f6d8ecf27fe772b748c7b2f3108558fbc23e8a)
#### [v0.42.82](https://github.com/RhetTbull/osxphotos/compare/v0.42.80...v0.42.82)
> 14 September 2021
- Fix for #515 [`93bf0c2`](https://github.com/RhetTbull/osxphotos/commit/93bf0c210cf01f351611427662025c86955ac373)
- Fix for #515, updated tests [`59c31ff`](https://github.com/RhetTbull/osxphotos/commit/59c31ff88d099b251cf1b571279d7a28a0aac138)
- Updated docs [`773dca8`](https://github.com/RhetTbull/osxphotos/commit/773dca849424c61a7447cb1bb87140708ab0a07c)
#### [v0.42.80](https://github.com/RhetTbull/osxphotos/compare/v0.42.79...v0.42.80)
> 29 August 2021
- Bug fix for null title, #512 [`6bcc676`](https://github.com/RhetTbull/osxphotos/commit/6bcc67634ca50e84494539b8a25eb7925dcede62)
- Updated dependencies [`2eb6e70`](https://github.com/RhetTbull/osxphotos/commit/2eb6e70e57ff1dc79907a29618757953f5871145)
- Updated README [skip ci] [`81dd1a7`](https://github.com/RhetTbull/osxphotos/commit/81dd1a753062dacc83aaf4ce8a7667de2cda599b)
#### [v0.42.79](https://github.com/RhetTbull/osxphotos/compare/v0.42.78...v0.42.79)
> 29 August 2021
#### [v0.42.78](https://github.com/RhetTbull/osxphotos/compare/v0.42.77...v0.42.78)
> 29 August 2021
- docs: add dssinger as a contributor for bug [`#514`](https://github.com/RhetTbull/osxphotos/pull/514)
- Fix for newlines in exif tags, #513 [`f0d7496`](https://github.com/RhetTbull/osxphotos/commit/f0d7496bc66aae291337efc570a2e2c4b9b5529c)
#### [v0.42.77](https://github.com/RhetTbull/osxphotos/compare/v0.42.74...v0.42.77)
> 28 August 2021

View File

@@ -1,5 +1,8 @@
include osxphotos/*.json
include osxphotos/*.md
include osxphotos/phototemplate.tx
include osxphotos/queries/*
include osxphotos/templates/*
include README.md
include README.rst
include osxphotos/templates/*
include osxphotos/phototemplate.tx
include osxphotos/phototemplate.md
recursive-include osxphotos/docs *

1199
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ If you have access to macOS 12 / Monterey beta and would like to help ensure osx
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.
Requires python >= ``3.7``.
Requires python >= ``3.8``.
Installation
------------
@@ -86,32 +86,40 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
Options:
--db <Photos database path> Specify Photos database path. Path to Photos
library/database can be specified using either
--db or directly as PHOTOS_LIBRARY positional
argument. If neither --db or PHOTOS_LIBRARY
provided, will attempt to find the library to
use in the following order: 1. last opened
library, 2. system library, 3.
~/Pictures/Photos Library.photoslibrary
--json Print output in JSON format.
library/database can be specified using either
--db or directly as PHOTOS_LIBRARY positional
argument. If neither --db or PHOTOS_LIBRARY
provided, will attempt to find the library to
use in the following order: 1. last opened
library, 2. system library, 3.
~/Pictures/Photos Library.photoslibrary
--json Print output in JSON format.
-v, --version Show the version and exit.
-h, --help Show this message and exit.
Commands:
about Print information about osxphotos including license.
albums Print out albums found in the Photos library.
dump Print list of all photos & associated info from the Photos...
export Export photos from the Photos database.
help Print help; for help on commands: help <command>.
info Print out descriptive info of the Photos library database.
keywords Print out keywords found in the Photos library.
labels Print out image classification labels found in the Photos...
list Print list of Photos libraries found on the system.
persons Print out persons (faces) found in the Photos library.
places Print out places found in the Photos library.
query Query the Photos database using 1 or more search options; if...
repl Run interactive osxphotos shell
tutorial Display osxphotos tutorial.
about Print information about osxphotos including license.
albums Print out albums found in the Photos library.
diff Compare two Photos databases and print out differences
docs Open osxphotos documentation in your browser.
dump Print list of all photos & associated info from the Photos...
export Export photos from the Photos database.
help Print help; for help on commands: help <command>.
info Print out descriptive info of the Photos library database.
install Install Python packages into the same environment as osxphotos
keywords Print out keywords found in the Photos library.
labels Print out image classification labels found in the Photos...
list Print list of Photos libraries found on the system.
persons Print out persons (faces) found in the Photos library.
places Print out places found in the Photos library.
query Query the Photos database using 1 or more search options; if...
repl Run interactive osxphotos REPL shell (useful for debugging,...
run Run a python file using same environment as osxphotos
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos
To get help on a specific command, use ``osxphotos help <command_name>``

View File

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

4
cli.py
View File

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

View File

@@ -1,7 +1,13 @@
pytest==6.2.4
pytest-mock==3.6.1
Sphinx==4.0.2
sphinx-rtd-theme==0.5.2
wheel==0.36.2
twine==3.4.1
pyinstaller==4.3
Sphinx
build
m2r2
pdbpp
pyinstaller==4.10
pytest-mock
pytest==7.0.1
sphinx_click
sphinx_rtd_theme
sphinxcontrib-programoutput
sphinxcontrib-programoutput
twine
wheel

View File

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

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.42.69 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.47.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>
@@ -31,11 +31,7 @@
<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>
<ul><li><a href="osxphotos/photoinfo.html">osxphotos.photoinfo</a></li>
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
</ul>
@@ -71,7 +67,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -93,7 +89,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.43.6 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -89,7 +89,7 @@
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">findfiles</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">noop</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">increment_filename_with_count</span><span class="p">,</span> <span class="n">lineno</span>
<span class="c1"># retry if use_photos_export fails the first time (which sometimes it does)</span>
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
@@ -313,7 +313,7 @@
<span class="k">if</span> <span class="ow">not</span> <span class="n">exported_files</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">filename</span><span class="p">:</span>
<span class="c1"># nothing got exported</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)&quot;</span><span class="p">)</span>
<span class="c1"># need to find actual filename as sometimes Photos renames JPG to jpeg on export</span>
<span class="c1"># may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)</span>
@@ -563,6 +563,7 @@
<span class="n">preview</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">preview_suffix</span><span class="o">=</span><span class="n">DEFAULT_PREVIEW_SUFFIX</span><span class="p">,</span>
<span class="n">render_options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span>
<span class="sd"> dest: must be valid destination path or exception raised</span>
@@ -621,6 +622,7 @@
<span class="sd"> preview: if True, also exports preview image</span>
<span class="sd"> preview_suffix: optional string to append to end of filename for preview images</span>
<span class="sd"> render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> Returns: ExportResults class</span>
<span class="sd"> ExportResults has attributes:</span>
@@ -714,15 +716,12 @@
<span class="c1"># e.g. exporting sidecar for file1.png and file1.jpeg</span>
<span class="c1"># if file1.png exists and exporting file1.jpeg,</span>
<span class="c1"># dest will be file1 (1).jpeg even though file1.jpeg doesn&#39;t exist to prevent sidecar collision</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span>
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">increment_file_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_original</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
<span class="n">dest_original</span>
<span class="p">)</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_original</span><span class="p">)</span>
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
<span class="k">if</span> <span class="p">(</span>
@@ -737,17 +736,11 @@
<span class="p">)</span>
<span class="k">if</span> <span class="n">export_edited</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span>
<span class="k">if</span> <span class="n">count</span><span class="p">:</span>
<span class="c1"># incremented above when checking original destination</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span>
<span class="p">)</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_edited</span><span class="p">)</span>
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
<span class="k">if</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">increment</span><span class="p">:</span>
@@ -831,20 +824,16 @@
<span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="c1"># not the right file, find the right one</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (*</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">break</span>
<span class="k">elif</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">cmp</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">file_</span><span class="p">):</span>
<span class="c1"># files match, update the UUID</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="p">,</span>
<span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
@@ -856,18 +845,14 @@
<span class="n">exif_json</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">break</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">found_match</span><span class="p">:</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># increment the destination file</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">stem</span>
<span class="k">while</span> <span class="n">dest_new</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
<span class="k">if</span> <span class="n">export_original</span><span class="p">:</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest</span>
<span class="c1"># export the dest file</span>
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
@@ -960,6 +945,14 @@
<span class="n">preview_path</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="n">preview_ext</span> <span class="o">=</span> <span class="n">preview_path</span><span class="o">.</span><span class="n">suffix</span>
<span class="n">preview_name</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}{</span><span class="n">preview_suffix</span><span class="si">}{</span><span class="n">preview_ext</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="c1"># if original is missing, the filename won&#39;t have been incremented so</span>
<span class="c1"># need to check here to make sure there aren&#39;t duplicate preview files in</span>
<span class="c1"># the export directory</span>
<span class="n">preview_name</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">preview_name</span>
<span class="k">if</span> <span class="n">overwrite</span> <span class="ow">or</span> <span class="n">update</span>
<span class="k">else</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">preview_name</span><span class="p">))</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">preview_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
<span class="n">preview_path</span><span class="p">,</span>
@@ -1002,6 +995,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1028,6 +1022,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1050,6 +1045,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1120,6 +1116,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
@@ -1143,6 +1140,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1163,6 +1161,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1188,6 +1187,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1208,6 +1208,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1298,6 +1299,7 @@
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_CURRENT</span><span class="p">,</span>
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
<span class="n">video</span><span class="o">=</span><span class="n">live_photo</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
@@ -1345,6 +1347,7 @@
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_ORIGINAL</span><span class="p">,</span>
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
<span class="n">video</span><span class="o">=</span><span class="n">live_photo</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
@@ -1554,16 +1557,31 @@
<span class="n">edited_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dest_exists</span> <span class="ow">and</span> <span class="p">(</span><span class="n">update</span> <span class="ow">or</span> <span class="n">overwrite</span><span class="p">):</span>
<span class="c1"># need to remove the destination first</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Error removing file </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> ((</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
<span class="k">if</span> <span class="n">export_as_hardlink</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Error hardlinking </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
<span class="k">elif</span> <span class="n">convert_to_jpeg</span><span class="p">:</span>
<span class="c1"># use convert_to_jpeg to export the file</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">convert_to_jpeg</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">,</span> <span class="n">compression_quality</span><span class="o">=</span><span class="n">jpeg_quality</span><span class="p">)</span>
<span class="n">converted_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
<span class="n">converted_to_jpeg_files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Error copying file </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_str</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest_str</span><span class="p">,</span>
@@ -1613,6 +1631,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span>
@@ -1626,6 +1645,7 @@
<span class="sd"> persons: if True, write person data to metadata</span>
<span class="sd"> location: if True, write location data to metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip any leading or trailing whitespace from rendered templates</span>
<span class="sd"> Returns:</span>
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
@@ -1643,6 +1663,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
@@ -1668,6 +1689,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1684,6 +1706,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip any rendered templates</span>
<span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1731,6 +1754,8 @@
<span class="p">)</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
@@ -1778,6 +1803,9 @@
<span class="p">)</span>
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
<span class="c1"># filter out any template values that didn&#39;t match by looking for sentinel</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">keyword</span>
@@ -1884,12 +1912,6 @@
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span>
<span class="c1"># remove any new lines in any fields</span>
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
<span class="k">return</span> <span class="n">exif</span>
@@ -1942,6 +1964,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1959,6 +1982,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1998,6 +2022,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
@@ -2023,6 +2048,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span>
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
@@ -2035,6 +2061,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
@@ -2052,6 +2079,8 @@
<span class="p">)</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
@@ -2093,6 +2122,9 @@
<span class="p">)</span>
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
<span class="c1"># filter out any template values that didn&#39;t match by looking for sentinel</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">keyword</span>
@@ -2180,7 +2212,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -2202,7 +2234,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.43.6 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -47,6 +47,7 @@
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">import</span> <span class="nn">yaml</span>
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
@@ -67,9 +68,11 @@
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
<span class="kn">from</span> <span class="nn">..momentinfo</span> <span class="kn">import</span> <span class="n">MomentInfo</span>
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
<span class="kn">from</span> <span class="nn">..query_builder</span> <span class="kn">import</span> <span class="n">get_query</span>
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
@@ -525,6 +528,18 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">moment</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Moment photo belongs to&quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_moment</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="n">MomentInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">,</span> <span class="n">moment_pk</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;momentID&quot;</span><span class="p">])</span>
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_moment</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;list of albums picture is contained in&quot;&quot;&quot;</span>
@@ -596,7 +611,12 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;name / title of picture&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span>
<span class="c1"># if user sets then deletes title, Photos sets it to empty string in DB instead of NULL</span>
<span class="c1"># in this case, return None so result is the same as if title had never been set (which returns NULL)</span>
<span class="c1"># issue #512</span>
<span class="n">title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span>
<span class="n">title</span> <span class="o">=</span> <span class="kc">None</span> <span class="k">if</span> <span class="n">title</span> <span class="o">==</span> <span class="s2">&quot;&quot;</span> <span class="k">else</span> <span class="n">title</span>
<span class="k">return</span> <span class="n">title</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -869,7 +889,7 @@
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
<span class="n">live_model_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;live_model_id&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="n">live_model_id</span> <span class="o">==</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">if</span> <span class="n">live_model_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
@@ -890,28 +910,20 @@
<span class="c1"># photos 4 has &quot;isOnDisk&quot; column we could check</span>
<span class="c1"># or could do the actual check with &quot;isfile&quot;</span>
<span class="c1"># TODO: should this be a warning or debug?</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist&quot;</span>
<span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># Photos 5</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov&quot;</span><span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
<span class="c1"># In testing, I&#39;ve seen occasional missing movie for live photo</span>
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
<span class="c1"># TODO: should this be a warning or debug?</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist&quot;</span>
<span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov&quot;</span><span class="p">)</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
<span class="c1"># In testing, I&#39;ve seen occasional missing movie for live photo</span>
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
<span class="c1"># TODO: should this be a warning or debug?</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">photopath</span>
@@ -1081,15 +1093,15 @@
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns height of the original photo version in pixels&quot;&quot;&quot;</span>
@@ -1125,6 +1137,26 @@
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Did not find signature for </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> in _db_signatures&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="n">duplicates</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">owner</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return name of photo owner for shared photos (Photos 5+ only), or None if not shared&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">personid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_hashed_person_id</span><span class="p">[</span><span class="n">personid</span><span class="p">][</span><span class="s2">&quot;full_name&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="n">personid</span>
<span class="k">else</span> <span class="kc">None</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
<div class="viewcode-block" id="PhotoInfo.render_template"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotoInfo.render_template">[docs]</a> <span class="k">def</span> <span class="nf">render_template</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="p">):</span>
@@ -1151,6 +1183,28 @@
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">()</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Error detecting text in photo </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span></div>
<span class="k">def</span> <span class="nf">_detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;detect text in photo, either from cached extended attribute or by attempting text detection&quot;&quot;&quot;</span>
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
<span class="p">)</span>
@@ -1158,23 +1212,13 @@
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span></div>
<span class="n">md</span> <span class="o">=</span> <span class="n">OSXMetaData</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">get_attribute</span><span class="p">(</span><span class="s2">&quot;osxphotos_detected_text&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">detected_text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span> <span class="ow">or</span> <span class="kc">None</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">orientation</span><span class="p">)</span>
<span class="n">md</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&quot;osxphotos_detected_text&quot;</span><span class="p">,</span> <span class="n">detected_text</span><span class="p">)</span>
<span class="k">return</span> <span class="n">detected_text</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1433,7 +1477,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -1455,7 +1499,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
osxphotos command line interface (CLI)
======================================
.. click:: osxphotos.cli:cli
.. click:: osxphotos.cli:cli_main
:prog: osxphotos
:nested: full
:nested: full
.. program-output:: python3 -m osxphotos --help

View File

@@ -4,7 +4,7 @@
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@@ -731,8 +731,9 @@ dl.glossary dt {
.classifier:before {
font-style: normal;
margin: 0.5em;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
@@ -756,6 +757,7 @@ span.pre {
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
white-space: nowrap;
}
div[class*="highlight-"] {
@@ -819,7 +821,7 @@ div.code-block-caption code {
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */

View File

@@ -4,7 +4,7 @@
*
* Sphinx JavaScript utilities for all documentation.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@@ -264,6 +264,9 @@ var Documentation = {
hideSearchWords : function() {
$('#searchbox .highlight-link').fadeOut(300);
$('span.highlighted').removeClass('highlighted');
var url = new URL(window.location);
url.searchParams.delete('highlight');
window.history.replaceState({}, '', url);
},
/**
@@ -301,12 +304,14 @@ var Documentation = {
window.location.href = prevHref;
return false;
}
break;
case 39: // right
var nextHref = $('link[rel="next"]').prop('href');
if (nextHref) {
window.location.href = nextHref;
return false;
}
break;
}
}
});

View File

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

View File

@@ -5,7 +5,7 @@
* 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.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/

View File

@@ -4,7 +4,7 @@
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@@ -282,7 +282,10 @@ var Search = {
complete: function(jqxhr, textstatus) {
var data = jqxhr.responseText;
if (data !== '' && data !== undefined) {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
var summary = Search.makeSearchSummary(data, searchterms, hlterms);
if (summary) {
listItem.append(summary);
}
}
Search.output.append(listItem);
setTimeout(function() {
@@ -325,7 +328,9 @@ var Search = {
var results = [];
for (var prefix in objects) {
for (var name in objects[prefix]) {
for (var iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
var match = objects[prefix][iMatch];
var name = match[4];
var fullname = (prefix ? prefix + '.' : '') + name;
var fullnameLower = fullname.toLowerCase()
if (fullnameLower.indexOf(object) > -1) {
@@ -339,7 +344,6 @@ var Search = {
} else if (parts[parts.length - 1].indexOf(object) > -1) {
score += Scorer.objPartialMatch;
}
var match = objects[prefix][name];
var objname = objnames[match[1]][2];
var title = titles[match[0]];
// If more than one term searched for, we require other words to be
@@ -498,6 +502,9 @@ var Search = {
*/
makeSearchSummary : function(htmlText, keywords, hlwords) {
var text = Search.htmlToText(htmlText);
if (text == "") {
return null;
}
var textLower = text.toLowerCase();
var start = 0;
$.each(keywords, function() {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.69 documentation</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.47.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>
@@ -31,30 +32,30 @@
<div class="body" role="main">
<div class="section" id="welcome-to-osxphotos-s-documentation">
<section id="welcome-to-osxphotos-s-documentation">
<h1>Welcome to osxphotoss documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline"></a></h1>
</div>
<div class="section" id="osxphotos">
</section>
<section id="osxphotos">
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="section" id="what-is-osxphotos">
<section id="what-is-osxphotos">
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline"></a></h2>
<p>OSXPhotos provides both the ability to interact with and query Apples Photos.app library on macOS directly from your python code
as well as a very flexible command line interface (CLI) app for exporting photos.
You can query the Photos library database for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
You can also easily export both the original and edited photos.</p>
</div>
<div class="section" id="supported-operating-systems">
</section>
<section id="supported-operating-systems">
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline"></a></h2>
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).</p>
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
<p>This package will read Photos databases for any supported version on any supported macOS version.
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
<p>Requires python &gt;= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
</div>
<div class="section" id="installation">
<p>Requires python &gt;= <code class="docutils literal notranslate"><span class="pre">3.8</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>
<div class="section" id="installation-using-pipx">
<section id="installation-using-pipx">
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline"></a></h3>
<p>If you arent familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
<ul class="simple">
@@ -64,15 +65,15 @@ E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
</ul>
</div>
<div class="section" id="installation-using-pip">
</section>
<section id="installation-using-pip">
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline"></a></h3>
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
</pre></div>
</div>
</div>
<div class="section" id="installation-from-git-repository">
</section>
<section id="installation-from-git-repository">
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline"></a></h3>
<p>OSXPhotos uses setuptools, thus simply run:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
@@ -87,9 +88,9 @@ I recommend you install the latest version from <a class="reference external" hr
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
If you arent comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
</div>
</div>
<div class="section" id="command-line-usage">
</section>
</section>
<section id="command-line-usage">
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline"></a></h2>
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
@@ -98,67 +99,75 @@ Alternatively, you can also run the command line utility like this: <code class=
<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="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>
<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">diff</span> <span class="n">Compare</span> <span class="n">two</span> <span class="n">Photos</span> <span class="n">databases</span> <span class="ow">and</span> <span class="nb">print</span> <span class="n">out</span> <span class="n">differences</span>
<span class="n">docs</span> <span class="n">Open</span> <span class="n">osxphotos</span> <span class="n">documentation</span> <span class="ow">in</span> <span class="n">your</span> <span class="n">browser</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">install</span> <span class="n">Install</span> <span class="n">Python</span> <span class="n">packages</span> <span class="n">into</span> <span class="n">the</span> <span class="n">same</span> <span class="n">environment</span> <span class="k">as</span> <span class="n">osxphotos</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">REPL</span> <span class="n">shell</span> <span class="p">(</span><span class="n">useful</span> <span class="k">for</span> <span class="n">debugging</span><span class="p">,</span><span class="o">...</span>
<span class="n">run</span> <span class="n">Run</span> <span class="n">a</span> <span class="n">python</span> <span class="n">file</span> <span class="n">using</span> <span class="n">same</span> <span class="n">environment</span> <span class="k">as</span> <span class="n">osxphotos</span>
<span class="n">snap</span> <span class="n">Create</span> <span class="n">snapshot</span> <span class="n">of</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">to</span> <span class="n">use</span> <span class="k">with</span> <span class="n">diff</span> <span class="n">command</span>
<span class="n">theme</span> <span class="n">Manage</span> <span class="n">osxphotos</span> <span class="n">color</span> <span class="n">themes</span><span class="o">.</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>
<span class="n">uninstall</span> <span class="n">Uninstall</span> <span class="n">Python</span> <span class="n">packages</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">osxphotos</span> <span class="n">environment</span>
<span class="n">uuid</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">unique</span> <span class="n">IDs</span> <span class="p">(</span><span class="n">UUID</span><span class="p">)</span> <span class="n">of</span> <span class="n">photos</span> <span class="n">selected</span> <span class="ow">in</span> <span class="n">Photos</span>
</pre></div>
</div>
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">&lt;command_name&gt;</span></code></p>
<div class="section" id="command-line-examples">
<section id="command-line-examples">
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline"></a></h3>
<div class="section" id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
<section id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
<h4>export all photos to ~/Desktop/export group in folders by date created<a class="headerlink" href="#export-all-photos-to-desktop-export-group-in-folders-by-date-created" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
<p><strong>Note</strong>: Photos library/database path can also be specified using <code class="docutils literal notranslate"><span class="pre">--db</span></code> option:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">--db</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
</div>
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
</section>
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">&gt;results.json</span></code></p>
</div>
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
<h4>export photos to file structure based on 4-digit year and full name of month of photos creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{created.month}&quot;</span></code></p>
<p>(by default, it will attempt to use the system library)</p>
</div>
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
<h4>export photos to file structure based on 4-digit year of photos creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{media_type}&quot;</span></code></p>
</div>
<div class="section" id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
</section>
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
<h4>export default library using country name/year as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{place.name.country,NoCountry}/{created.year}&quot;</span>&#160; <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
</div>
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
</section>
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">&quot;Big</span> <span class="pre">Videos&quot;</span></code></p>
</div>
</div>
</div>
<div class="section" id="example-uses-of-the-package">
</section>
</section>
</section>
<section id="example-uses-of-the-package">
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline"></a></h2>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Simple usage of the package &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
@@ -274,8 +283,8 @@ Alternatively, you can also run the command line utility like this: <code class=
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
</pre></div>
</div>
</div>
<div class="section" id="package-interface">
</section>
<section id="package-interface">
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline"></a></h2>
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
<div class="toctree-wrapper compound">
@@ -284,10 +293,12 @@ Alternatively, you can also run the command line utility like this: <code class=
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-diff">diff</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-install">install</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
@@ -295,7 +306,12 @@ Alternatively, you can also run the command line utility like this: <code class=
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-repl">repl</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-run">run</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-snap">snap</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-theme">theme</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-uninstall">uninstall</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-uuid">uuid</a></li>
</ul>
</li>
</ul>
@@ -306,16 +322,16 @@ Alternatively, you can also run the command line utility like this: <code class=
</li>
</ul>
</div>
</div>
</div>
<div class="section" id="indices-and-tables">
</section>
</section>
<section id="indices-and-tables">
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline"></a></h1>
<ul class="simple">
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
</ul>
</div>
</section>
</div>
@@ -351,7 +367,7 @@ Alternatively, you can also run the command line utility like this: <code class=
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -373,7 +389,7 @@ Alternatively, you can also run the command line utility like this: <code class=
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

View File

@@ -4,8 +4,9 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.42.69 documentation</title>
<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.47.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>
@@ -30,11 +31,11 @@
<div class="body" role="main">
<div class="section" id="osxphotos">
<section id="osxphotos">
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="toctree-wrapper compound">
</div>
</div>
</section>
</div>
@@ -69,7 +70,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -91,7 +92,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,10 @@ github:
@make html
@cp -a _build/html/. ../docs
docs:
@make html
@cp -a _build/html/. ../osxphotos/docs
pdf:
@make latexpdf
@cp -a _build/latex/osxphotos.pdf ../docs

View File

@@ -1,6 +1,8 @@
osxphotos command line interface (CLI)
======================================
.. click:: osxphotos.cli:cli
.. click:: osxphotos.cli:cli_main
:prog: osxphotos
:nested: full
:nested: full
.. program-output:: python3 -m osxphotos --help

View File

@@ -16,7 +16,7 @@ import sys
import sphinx_rtd_theme
sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information -----------------------------------------------------
@@ -30,9 +30,7 @@ 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:
with open(version_file, mode="r", encoding="utf-8") as f:
exec(f.read(), about)
# The full version, including alpha/beta/rc tags
@@ -44,7 +42,15 @@ release = about["__version__"]
# 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"]
extensions = [
"sphinx_click",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
"m2r2",
"sphinxcontrib.programoutput",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -66,4 +72,3 @@ html_theme = "alabaster"
# 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"]

View File

@@ -1,10 +1,12 @@
""" Example function for use with osxphotos export --post-function option """
from osxphotos import PhotoInfo, ExportResults
from typing import Callable
from osxphotos import ExportResults, PhotoInfo
def post_function(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
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

View File

@@ -2,19 +2,28 @@
# spec file for pyinstaller
# run `pyinstaller osxphotos.spec`
import os
import importlib
pathex = os.getcwd()
from PyInstaller.utils.hooks import collect_data_files
# include necessary data files
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"),
]
datas = collect_data_files("osxphotos")
datas.extend(
[
("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"),
("osxphotos/exiftool_filetypes.json", "osxphotos"),
("osxphotos/docs", "osxphotos/docs"),
]
)
package_imports = [["photoscript", ["photoscript.applescript"]]]
for package, files in package_imports:
proot = os.path.dirname(importlib.import_module(package).__file__)

View File

@@ -1,12 +1,50 @@
import logging
from ._constants import AlbumSortOrder
from ._version import __version__
from .debug import is_debug, set_debug
from .exiftool import ExifTool
from .photoinfo import ExportResults, PhotoInfo
from .export_db import ExportDB
from .fileutil import FileUtil, FileUtilNoOp
from .momentinfo import MomentInfo
from .personinfo import PersonInfo
from .photoexporter import ExportOptions, ExportResults, PhotoExporter
from .photoinfo import PhotoInfo
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototemplate import PhotoTemplate
from .placeinfo import PlaceInfo
from .queryoptions import QueryOptions
from .utils import _debug, _get_logger, _set_debug
from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo
from .utils import _get_logger
# TODO: Add test for imageTimeZoneOffsetSeconds = None
# TODO: Add special albums and magic albums
if not is_debug():
logging.disable(logging.DEBUG)
__all__ = [
"__version__",
"_get_logger",
"AlbumSortOrder",
"CommentInfo",
"ExifTool",
"ExportDB",
"ExportDBTemp",
"ExportOptions",
"ExportResults",
"FileUtil",
"FileUtilNoOp",
"is_debug",
"LikeInfo",
"MomentInfo",
"PersonInfo",
"PhotoExporter",
"PhotoInfo",
"PhotosDB",
"PhotoTemplate",
"PlaceInfo",
"QueryOptions",
"ScoreInfo",
"SearchInfo",
"set_debug",
]

View File

@@ -1,6 +1,6 @@
"""Command line interface for osxphotos """
from .cli import cli
from .cli.cli import cli_main
if __name__ == "__main__":
cli() # pylint: disable=no-value-for-parameter
cli_main()

View File

@@ -6,6 +6,8 @@ import os.path
from datetime import datetime
from enum import Enum
APP_NAME = "osxphotos"
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
# Time delta: add this to Photos times to get unix time
@@ -20,8 +22,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
@@ -30,17 +32,22 @@ _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.7 (also Big Sur and Monterey which switch to model version)
_PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common on Catalina and up but there are some version 5001 database in the wild
# Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134
_PHOTOS_7_MODEL_VERSION = [
15000,
15999,
] # Monterey developer preview is 15134, 12.1 is 15331
# some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = {
@@ -94,6 +101,12 @@ _TESTED_OS_VERSIONS = [
("11", "2"),
("11", "3"),
("11", "4"),
("11", "5"),
("11", "6"),
("12", "0"),
("12", "1"),
("12", "2"),
("12", "3"),
]
# Photos 5 has persons who are empty string if unidentified face
@@ -119,12 +132,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
@@ -214,10 +235,6 @@ 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"
# Bit masks for --sidecar
SIDECAR_JSON = 0x1
SIDECAR_EXIFTOOL = 0x2
@@ -242,10 +259,12 @@ EXTENDED_ATTRIBUTE_NAMES = [
]
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
@@ -275,12 +294,33 @@ POST_COMMAND_CATEGORIES = {
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
}
class AlbumSortOrder(Enum):
"""Album Sort Order"""
UNKNOWN = 0
MANUAL = 1
NEWEST_FIRST = 2
OLDEST_FIRST = 3
TITLE = 5
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
# stat sort order for cProfile: https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats
PROFILE_SORT_KEYS = [
"calls",
"cumulative",
"cumtime",
"file",
"filename",
"module",
"ncalls",
"pcalls",
"line",
"name",
"nfl",
"stdname",
"time",
"tottime",
]

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.78"
__version__ = "0.47.8"

View File

@@ -16,6 +16,8 @@ import zlib
from .datetime_utils import datetime_naive_to_utc
__all__ = ["AdjustmentsDecodeError", "AdjustmentsInfo"]
class AdjustmentsDecodeError(Exception):
"""Could not decode adjustments plist file"""
@@ -73,37 +75,37 @@ class AdjustmentsInfo:
@property
def plist(self):
"""The actual adjustments plist content as a dict """
"""The actual adjustments plist content as a dict"""
return self._plist
@property
def data(self):
"""The raw adjustments data as a binary blob """
"""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 """
"""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 """
"""The value of the adjustmentFormatIdentifier field in the plist"""
return self._format_identifier
@property
def base_version(self):
"""Value of adjustmentBaseVersion field """
"""Value of adjustmentBaseVersion field"""
return self._base_version
@property
def format_version(self):
"""The value of the adjustmentFormatVersion in the plist """
"""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 """
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp"""
return self._timestamp
@property

View File

@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
from ._constants import (
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
@@ -22,6 +22,16 @@ from ._constants import (
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .query_builder import get_query
__all__ = [
"sort_list_by_keys",
"AlbumInfoBaseClass",
"AlbumInfo",
"ImportInfo",
"ProjectInfo",
"FolderInfo",
]
def sort_list_by_keys(values, sort_keys):
@@ -131,6 +141,28 @@ class AlbumInfoBaseClass:
def photos(self):
return []
@property
def owner(self):
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
try:
return self._owner
except AttributeError:
try:
personid = self._db._dbalbum_details[self.uuid][
"cloudownerhashedpersonid"
]
self._owner = (
self._db._db_hashed_person_id[personid]["full_name"]
if personid
else None
)
except KeyError:
self._owner = None
return self._owner
def __len__(self):
"""return number of photos contained in album"""
return len(self.photos)
@@ -138,7 +170,6 @@ class AlbumInfoBaseClass:
class AlbumInfo(AlbumInfoBaseClass):
"""
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
@@ -208,7 +239,7 @@ class AlbumInfo(AlbumInfoBaseClass):
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
self._parent = (
FolderInfo(db=self._db, uuid=parent_uuid)
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
else None
)
else:
@@ -243,18 +274,17 @@ class AlbumInfo(AlbumInfoBaseClass):
def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
index = 0
for p in self.photos:
for index, p in enumerate(self.photos):
if p.uuid == photo.uuid:
return index
index += 1
else:
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
class ImportInfo(AlbumInfoBaseClass):
"""Information about import sessions"""
@property
def photos(self):
"""return list of photos contained in import session"""
@@ -273,6 +303,15 @@ class ImportInfo(AlbumInfoBaseClass):
return self._photos
class ProjectInfo(AlbumInfo):
"""
ProjectInfo with info about projects
Projects are cards, calendars, slideshows, etc.
"""
...
class FolderInfo:
"""
Info about a specific folder, contains all the details about the folder
@@ -334,7 +373,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:

97
osxphotos/cli/__init__.py Normal file
View File

@@ -0,0 +1,97 @@
"""cli package for osxphotos"""
import sys
from rich import print
from rich.traceback import install as install_traceback
from osxphotos.debug import (
debug_breakpoint,
debug_watch,
get_debug_flags,
get_debug_options,
set_debug,
wrap_function,
)
# apply any debug functions
# need to do this before importing anything else so that the debug functions
# wrap the right function references
# if a module does something like "from exiftool import ExifTool" and the user tries
# to wrap 'osxphotos.exiftool.ExifTool.asdict', the original ExifTool.asdict will be
# wrapped but the caller will have a reference to the function before it was wrapped
# reference: https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/13-ordering-issues-when-monkey-patching-in-python.md
args = get_debug_options(["--watch", "--breakpoint"], sys.argv)
for func_name in args.get("--watch", []):
try:
wrap_function(func_name, debug_watch)
print(f"Watching {func_name}")
except AttributeError:
print(f"{func_name} does not exist")
sys.exit(1)
for func_name in args.get("--breakpoint", []):
try:
wrap_function(func_name, debug_breakpoint)
print(f"Breakpoint added for {func_name}")
except AttributeError:
print(f"{func_name} does not exist")
sys.exit(1)
args = get_debug_flags(["--debug"], sys.argv)
if args.get("--debug", False):
set_debug(True)
print("Debugging enabled")
from .about import about
from .albums import albums
from .cli import cli_main
from .common import get_photos_db, load_uuid_from_file
from .debug_dump import debug_dump
from .dump import dump
from .export import export
from .exportdb import exportdb
from .grep import grep
from .help import help
from .info import info
from .install_uninstall_run import install, run, uninstall
from .keywords import keywords
from .labels import labels
from .list import _list_libraries, list_libraries
from .persons import persons
from .places import places
from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .tutorial import tutorial
from .uuid import uuid
install_traceback()
__all__ = [
"about",
"albums",
"cli_main",
"debug_dump",
"diff",
"dump",
"export",
"exportdb",
"grep",
"help",
"info",
"install",
"keywords",
"labels",
"list_libraries",
"list_libraries",
"load_uuid_from_file",
"persons",
"places",
"query",
"repl",
"run",
"set_debug",
"snap",
"tutorial",
"uuid",
]

66
osxphotos/cli/about.py Normal file
View File

@@ -0,0 +1,66 @@
"""about command for osxphotos CLI"""
import click
from osxphotos._constants import OSXPHOTOS_URL
from osxphotos._version import __version__
@click.command(name="about")
@click.pass_obj
@click.pass_context
def about(ctx, cli_obj):
"""Print information about osxphotos including license."""
license = """
MIT License
Copyright (c) 2019-2021 Rhet Turnbull
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
osxphotos uses the following 3rd party software licensed under the BSD-3-Clause License:
Click (Copyright 2014 Pallets), ptpython (Copyright (c) 2015, Jonathan Slenders)
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be
used to endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
click.echo(f"osxphotos, version {__version__}")
click.echo("")
click.echo(f"Source code available at: {OSXPHOTOS_URL}")
click.echo(license)

42
osxphotos/cli/albums.py Normal file
View File

@@ -0,0 +1,42 @@
"""albums command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
from osxphotos._constants import _PHOTOS_4_VERSION
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def albums(ctx, cli_obj, db, json_, photos_library):
"""Print out albums found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["albums"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
albums = {"albums": photosdb.albums_as_dict}
if photosdb.db_version > _PHOTOS_4_VERSION:
albums["shared albums"] = photosdb.albums_shared_as_dict
if json_ or cli_obj.json:
click.echo(json.dumps(albums, ensure_ascii=False))
else:
click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True))

88
osxphotos/cli/cli.py Normal file
View File

@@ -0,0 +1,88 @@
"""Command line interface for osxphotos """
import click
import osxphotos
from osxphotos._version import __version__
from .about import about
from .albums import albums
from .common import DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN
from .debug_dump import debug_dump
from .docs import docs
from .dump import dump
from .export import export
from .exportdb import exportdb
from .grep import grep
from .help import help
from .info import info
from .install_uninstall_run import install, run, uninstall
from .keywords import keywords
from .labels import labels
from .list import _list_libraries, list_libraries
from .persons import persons
from .places import places
from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .theme import theme
from .tutorial import tutorial
from .uuid import uuid
# Click CLI object & context settings
class CLI_Obj:
def __init__(self, db=None, json=False, debug=False, group=None):
self.db = db
self.json = json
self.group = group
CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
@click.group(context_settings=CTX_SETTINGS)
@DB_OPTION
@JSON_OPTION
@click.option(
"--debug",
required=False,
is_flag=True,
help="Enable debug output",
hidden=OSXPHOTOS_HIDDEN,
)
@click.version_option(__version__, "--version", "-v")
@click.pass_context
def cli_main(ctx, db, json_, debug):
ctx.obj = CLI_Obj(db=db, json=json_, group=cli_main)
# install CLI commands
for command in [
about,
albums,
debug_dump,
diff,
docs,
dump,
export,
exportdb,
grep,
help,
info,
install,
keywords,
labels,
list_libraries,
persons,
places,
query,
repl,
run,
snap,
theme,
tutorial,
uninstall,
uuid,
]:
cli_main.add_command(command)

View File

@@ -0,0 +1,268 @@
"""click.echo replacement that supports rich text formatting"""
import inspect
import os
import typing as t
import click
from rich.console import Console
from rich.markdown import Markdown
from rich.theme import Theme
from .common import time_stamp
__all__ = [
"get_rich_console",
"get_rich_theme",
"rich_click_echo",
"rich_echo",
"rich_echo_error",
"rich_echo_via_pager",
"set_rich_console",
"set_rich_theme",
"set_rich_timestamp",
]
# TODO: this should really be a class instead of a module with a bunch of globals
# include emoji's in rich_echo_error output
ERROR_EMOJI = True
class _Console:
"""Store console object for rich output"""
def __init__(self):
self._console: t.Optional[Console] = None
@property
def console(self):
return self._console
@console.setter
def console(self, console: Console):
self._console = console
_console = _Console()
_theme = None
_timestamp = False
# set to 1 if running tests
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
def set_rich_console(console: Console) -> None:
"""Set the console object to use for rich_echo and rich_echo_via_pager"""
global _console
_console.console = console
def get_rich_console() -> Console:
"""Get console object
Returns:
Console object
"""
global _console
return _console.console
def set_rich_theme(theme: Theme) -> None:
"""Set the theme to use for rich_click_echo"""
global _theme
_theme = theme
def get_rich_theme() -> t.Optional[Theme]:
"""Get the theme to use for rich_click_echo"""
global _theme
return _theme
def set_rich_timestamp(timestamp: bool) -> None:
"""Set whether to print timestamp with rich_echo, rich_echo_error, and rich_click_error"""
global _timestamp
_timestamp = timestamp
def rich_echo(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting.
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
# args for click.echo that may have been passed in kwargs
echo_args = {}
for arg in ("file", "nl", "err", "color"):
val = kwargs.pop(arg, None)
if val is not None:
echo_args[arg] = val
width = kwargs.pop("width", None)
if width is None and OSXPHOTOS_IS_TESTING:
# if not outputting to terminal, use a huge width to avoid wrapping
# otherwise tests fail
width = 10_000
console = get_rich_console() or Console(theme=theme, width=width)
if markdown:
message = Markdown(message)
# Markdown always adds a new line so disable unless explicitly specified
global _timestamp
if _timestamp:
message = time_stamp() + message
console.print(message, highlight=highlight, **kwargs)
def rich_echo_error(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting and if stdout is redirected, echo to stderr
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
global ERROR_EMOJI
if ERROR_EMOJI:
if "[error]" in message:
message = f":cross_mark-emoji: {message}"
elif "[warning]" in message:
message = f":warning-emoji: {message}"
console = get_rich_console() or Console(theme=theme or get_rich_theme())
if not console.is_terminal:
# if stdout is redirected, echo to stderr
rich_click_echo(
message,
theme=theme or get_rich_theme(),
markdown=markdown,
highlight=highlight,
**kwargs,
err=True,
)
else:
rich_echo(
message,
theme=theme or get_rich_theme(),
markdown=markdown,
highlight=highlight,
**kwargs,
)
def rich_click_echo(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting using click.echo
This is a wrapper around click.echo that supports rich text formatting.
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
# args for click.echo that may have been passed in kwargs
echo_args = {}
for arg in ("file", "nl", "err", "color"):
val = kwargs.pop(arg, None)
if val is not None:
echo_args[arg] = val
# click.echo will include "\n" so don't add it here unless specified
end = kwargs.pop("end", "")
if width := kwargs.pop("width", None) is None:
# if not outputting to terminal, use a huge width to avoid wrapping
# otherwise tests fail
temp_console = Console()
width = temp_console.width if temp_console.is_terminal else 10_000
console = Console(
force_terminal=True,
theme=theme or get_rich_theme(),
width=width,
)
if markdown:
message = Markdown(message)
# Markdown always adds a new line so disable unless explicitly specified
echo_args["nl"] = echo_args.get("nl") is True
global _timestamp
if _timestamp:
message = time_stamp() + message
with console.capture() as capture:
console.print(message, end=end, highlight=highlight, **kwargs)
click.echo(capture.get(), **echo_args)
def rich_echo_via_pager(
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
theme: t.Optional[Theme] = None,
highlight=False,
markdown: bool = False,
**kwargs,
) -> None:
"""This function takes a text and shows it via an environment specific
pager on stdout.
Args:
text_or_generator: the text to page, or alternatively, a generator emitting the text to page.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
**kwargs: if "color" in kwargs, works the same as click.echo_via_pager(color=color)
otherwise any kwargs are passed to rich.Console.print()
"""
if inspect.isgeneratorfunction(text_or_generator):
text_or_generator = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
elif isinstance(text_or_generator, str):
text_or_generator = [text_or_generator]
else:
try:
text_or_generator = iter(text_or_generator)
except TypeError:
text_or_generator = [text_or_generator]
console = _console.console or Console(theme=theme)
color = kwargs.pop("color", True)
with console.pager(styles=color):
for x in text_or_generator:
if isinstance(x, str) and markdown:
x = Markdown(x)
console.print(x, highlight=highlight, **kwargs)

View File

@@ -0,0 +1,194 @@
"""Support for colorized output for osxphotos cli using rich"""
import pathlib
from typing import List, Optional
import click
from rich.style import Style
from rich_theme_manager import Theme, ThemeManager
from .common import get_config_dir, noop
from .darkmode import is_dark_mode
DEFAULT_THEME_NAME = "default"
__all__ = [
"get_default_theme",
"get_theme",
"get_theme_dir",
"get_theme_manager",
DEFAULT_THEME_NAME,
]
THEME_STYLES = [
"color",
"count",
"error",
"filename",
"filepath",
"highlight",
"num",
"time",
"uuid",
"warning",
"bar.back",
"bar.complete",
"bar.finished",
"bar.pulse",
"progress.elapsed",
"progress.percentage",
"progress.remaining",
]
COLOR_THEMES = {
"dark": Theme(
name="dark",
description="Dark mode theme",
tags=["dark"],
styles={
# color pallette from https://github.com/dracula/dracula-theme
"color": Style(color="rgb(248,248,242)"),
"count": Style(color="rgb(139,233,253)"),
"error": Style(color="rgb(255,85,85)", bold=True),
"filename": Style(color="rgb(189,147,249)", bold=True),
"filepath": Style(color="rgb(80,250,123)", bold=True),
"highlight": Style(color="#000000", bgcolor="#d73a49", bold=True),
"num": Style(color="rgb(139,233,253)", bold=True),
"time": Style(color="rgb(139,233,253)", bold=True),
"uuid": Style(color="rgb(255,184,108)"),
"warning": Style(color="rgb(241,250,140)", bold=True),
"bar.back": Style(color="rgb(68,71,90)"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(80,250,123)"),
"bar.pulse": Style(color="rgb(98,114,164)"),
"progress.elapsed": Style(color="rgb(139,233,253)"),
"progress.percentage": Style(color="rgb(255,121,198)"),
"progress.remaining": Style(color="rgb(139,233,253)"),
# "headers": Style(color="rgb(165,194,97)"),
# "options": Style(color="rgb(255,198,109)"),
# "metavar": Style(color="rgb(12,125,157)"),
},
),
"light": Theme(
name="light",
description="Light mode theme",
styles={
"color": Style(color="#000000"),
"count": Style(color="#005cc5", bold=True),
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
"filename": Style(color="#6f42c1", bold=True),
"filepath": Style(color="#22863a", bold=True),
"highlight": Style(color="#ffffff", bgcolor="#d73a49", bold=True),
"num": Style(color="#005cc5", bold=True),
"time": Style(color="#032f62", bold=True),
"uuid": Style(color="#d73a49", bold=True),
"warning": Style(color="#e36209", bold=True, underline=True, italic=True),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
"bar.pulse": Style(color="rgb(249,38,114)"),
"progress.elapsed": Style(color="#032f62", bold=True),
"progress.percentage": Style(color="#6f42c1", bold=True),
"progress.remaining": Style(color="#032f62", bold=True),
# "headers": Style(color="rgb(254,212,66)"),
# "options": Style(color="rgb(227,98,9)"),
# "metavar": Style(color="rgb(111,66,193)"),
},
),
"mono": Theme(
name="mono",
description="Monochromatic theme",
tags=["mono", "colorblind"],
styles={
"count": "bold",
"error": "reverse italic",
"filename": "bold",
"filepath": "bold underline",
"highlight": "reverse italic",
"num": "bold",
"time": "bold",
"uuid": "bold",
"warning": "bold italic",
"bar.back": "",
"bar.complete": "reverse",
"bar.finished": "bold",
"bar.pulse": "bold",
"progress.elapsed": "",
"progress.percentage": "bold",
"progress.remaining": "bold",
# "headers": "bold",
# "options": "bold",
# "metavar": "bold",
},
),
"plain": Theme(
name="plain",
description="Plain theme with no colors",
tags=["colorblind"],
styles={
"color": "",
"count": "",
"error": "",
"filename": "",
"filepath": "",
"highlight": "",
"num": "",
"time": "",
"uuid": "",
"warning": "",
"bar.back": "",
"bar.complete": "",
"bar.finished": "",
"bar.pulse": "",
"progress.elapsed": "",
"progress.percentage": "",
"progress.remaining": "",
# "headers": "",
# "options": "",
# "metavar": "",
},
),
}
def get_theme_dir() -> pathlib.Path:
"""Return the theme config dir, creating it if necessary"""
theme_dir = get_config_dir() / "themes"
if not theme_dir.exists():
theme_dir.mkdir()
return theme_dir
def get_theme_manager() -> ThemeManager:
"""Return theme manager instance"""
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
def get_theme(
theme_name: Optional[str] = None,
):
"""Get theme by name, or default theme if no name is provided"""
if theme_name is None:
return get_default_theme()
theme_manager = get_theme_manager()
try:
return theme_manager.get(theme_name)
except ValueError as e:
raise click.ClickException(
f"Theme '{theme_name}' not found. "
f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
) from e
def get_default_theme():
"""Get the default color theme"""
theme_manager = get_theme_manager()
try:
return theme_manager.get(DEFAULT_THEME_NAME)
except ValueError:
return (
theme_manager.get("dark") if is_dark_mode() else theme_manager.get("light")
)

546
osxphotos/cli/common.py Normal file
View File

@@ -0,0 +1,546 @@
"""Globals and constants use by the CLI commands"""
import os
import pathlib
from datetime import datetime
import click
import osxphotos
from osxphotos._constants import APP_NAME
from osxphotos._version import __version__
from .param_types import *
# used to show/hide hidden commands
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
# used by snap and diff commands
OSXPHOTOS_SNAPSHOT_DIR = "/private/tmp/osxphotos_snapshots"
# where to write the crash report if osxphotos crashes
OSXPHOTOS_CRASH_LOG = f"{os.getcwd()}/osxphotos_crash.log"
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
__all__ = [
"CLI_COLOR_ERROR",
"CLI_COLOR_WARNING",
"DB_ARGUMENT",
"DB_OPTION",
"DEBUG_OPTIONS",
"DELETED_OPTIONS",
"JSON_OPTION",
"QUERY_OPTIONS",
"THEME_OPTION",
"get_photos_db",
"load_uuid_from_file",
"noop",
"time_stamp",
]
def noop(*args, **kwargs):
"""no-op function"""
pass
def time_stamp() -> str:
"""return timestamp"""
return f"[time]{str(datetime.now())}[/time] -- "
def get_photos_db(*db_options):
"""Return path to photos db, select first non-None db_options
If no db_options are non-None, try to find library to use in
the following order:
- last library opened
- system library
- ~/Pictures/Photos Library.photoslibrary
- failing above, returns None
"""
if db_options:
for db in db_options:
if db is not None:
return db
# if get here, no valid database paths passed, so try to figure out which to use
db = osxphotos.utils.get_last_library_path()
if db is not None:
click.echo(f"Using last opened Photos library: {db}", err=True)
return db
db = osxphotos.utils.get_system_library_path()
if db is not None:
click.echo(f"Using system Photos library: {db}", err=True)
return db
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
if os.path.isdir(db):
click.echo(f"Using Photos library: {db}", err=True)
return db
else:
return None
DB_OPTION = click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help=(
"Specify Photos database path. "
"Path to Photos library/database can be specified using either --db "
"or directly as PHOTOS_LIBRARY positional argument. "
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
),
type=click.Path(exists=True),
)
DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
JSON_OPTION = click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
def DELETED_OPTIONS(f):
o = click.option
options = [
o(
"--deleted",
is_flag=True,
help="Include photos from the 'Recently Deleted' folder.",
),
o(
"--deleted-only",
is_flag=True,
help="Include only photos from the 'Recently Deleted' folder.",
),
]
for o in options[::-1]:
f = o(f)
return f
def QUERY_OPTIONS(f):
o = click.option
options = [
o(
"--keyword",
metavar="KEYWORD",
default=None,
multiple=True,
help="Search for photos with keyword KEYWORD. "
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
),
o(
"--person",
metavar="PERSON",
default=None,
multiple=True,
help="Search for photos with person PERSON. "
'If more than one person, treated as "OR", e.g. find photos matching any person',
),
o(
"--album",
metavar="ALBUM",
default=None,
multiple=True,
help="Search for photos in album ALBUM. "
'If more than one album, treated as "OR", e.g. find photos matching any album',
),
o(
"--folder",
metavar="FOLDER",
default=None,
multiple=True,
help="Search for photos in an album in folder FOLDER. "
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
"Only searches top level folders (e.g. does not look at subfolders)",
),
o(
"--name",
metavar="FILENAME",
default=None,
multiple=True,
help="Search for photos with filename matching FILENAME. "
'If more than one --name options is specified, they are treated as "OR", '
"e.g. find photos matching any FILENAME. ",
),
o(
"--uuid",
metavar="UUID",
default=None,
multiple=True,
help="Search for photos with UUID(s). "
"May be repeated to include multiple UUIDs.",
),
o(
"--uuid-from-file",
metavar="FILE",
default=None,
multiple=False,
help="Search for photos with UUID(s) loaded from FILE. "
"Format is a single UUID per line. Lines preceded with # are ignored.",
type=click.Path(exists=True),
),
o(
"--title",
metavar="TITLE",
default=None,
multiple=True,
help="Search for TITLE in title of photo.",
),
o("--no-title", is_flag=True, help="Search for photos with no title."),
o(
"--description",
metavar="DESC",
default=None,
multiple=True,
help="Search for DESC in description of photo.",
),
o(
"--no-description",
is_flag=True,
help="Search for photos with no description.",
),
o(
"--place",
metavar="PLACE",
default=None,
multiple=True,
help="Search for PLACE in photo's reverse geolocation info",
),
o(
"--no-place",
is_flag=True,
help="Search for photos with no associated place name info (no reverse geolocation info)",
),
o(
"--location",
is_flag=True,
help="Search for photos with associated location info (e.g. GPS coordinates)",
),
o(
"--no-location",
is_flag=True,
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
),
o(
"--label",
metavar="LABEL",
multiple=True,
help="Search for photos with image classification label LABEL (Photos 5 only). "
'If more than one label, treated as "OR", e.g. find photos matching any label',
),
o(
"--uti",
metavar="UTI",
default=None,
multiple=False,
help="Search for photos whose uniform type identifier (UTI) matches UTI",
),
o(
"-i",
"--ignore-case",
is_flag=True,
help="Case insensitive search for title, description, place, keyword, person, or album.",
),
o("--edited", is_flag=True, help="Search for photos that have been edited."),
o(
"--external-edit",
is_flag=True,
help="Search for photos edited in external editor.",
),
o("--favorite", is_flag=True, help="Search for photos marked favorite."),
o(
"--not-favorite",
is_flag=True,
help="Search for photos not marked favorite.",
),
o("--hidden", is_flag=True, help="Search for photos marked hidden."),
o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."),
o(
"--shared",
is_flag=True,
help="Search for photos in shared iCloud album (Photos 5 only).",
),
o(
"--not-shared",
is_flag=True,
help="Search for photos not in shared iCloud album (Photos 5 only).",
),
o(
"--burst",
is_flag=True,
help="Search for photos that were taken in a burst.",
),
o(
"--not-burst",
is_flag=True,
help="Search for photos that are not part of a burst.",
),
o("--live", is_flag=True, help="Search for Apple live photos"),
o(
"--not-live",
is_flag=True,
help="Search for photos that are not Apple live photos.",
),
o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."),
o(
"--not-portrait",
is_flag=True,
help="Search for photos that are not Apple portrait mode photos.",
),
o("--screenshot", is_flag=True, help="Search for screenshot photos."),
o(
"--not-screenshot",
is_flag=True,
help="Search for photos that are not screenshot photos.",
),
o("--slow-mo", is_flag=True, help="Search for slow motion videos."),
o(
"--not-slow-mo",
is_flag=True,
help="Search for photos that are not slow motion videos.",
),
o("--time-lapse", is_flag=True, help="Search for time lapse videos."),
o(
"--not-time-lapse",
is_flag=True,
help="Search for photos that are not time lapse videos.",
),
o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."),
o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."),
o(
"--selfie",
is_flag=True,
help="Search for selfies (photos taken with front-facing cameras).",
),
o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."),
o("--panorama", is_flag=True, help="Search for panorama photos."),
o(
"--not-panorama",
is_flag=True,
help="Search for photos that are not panoramas.",
),
o(
"--has-raw",
is_flag=True,
help="Search for photos with both a jpeg and raw version",
),
o(
"--only-movies",
is_flag=True,
help="Search only for movies (default searches both images and movies).",
),
o(
"--only-photos",
is_flag=True,
help="Search only for photos/images (default searches both images and movies).",
),
o(
"--from-date",
help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(),
),
o(
"--to-date",
help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(),
),
o(
"--from-time",
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
type=TimeISO8601(),
),
o(
"--to-time",
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
type=TimeISO8601(),
),
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
o(
"--is-reference",
is_flag=True,
help="Search for photos that were imported as referenced files (not copied into Photos library).",
),
o(
"--in-album",
is_flag=True,
help="Search for photos that are in one or more albums.",
),
o(
"--not-in-album",
is_flag=True,
help="Search for photos that are not in any albums.",
),
o(
"--duplicate",
is_flag=True,
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
"times or duplicated within Photos.",
),
o(
"--min-size",
metavar="SIZE",
type=BitMathSize(),
help="Search for photos with size >= SIZE bytes. "
"The size evaluated is the photo's original size (when imported to Photos). "
"Size may be specified as integer bytes or using SI or NIST units. "
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
),
o(
"--max-size",
metavar="SIZE",
type=BitMathSize(),
help="Search for photos with size <= SIZE bytes. "
"The size evaluated is the photo's original size (when imported to Photos). "
"Size may be specified as integer bytes or using SI or NIST units. "
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
),
o(
"--regex",
metavar="REGEX TEMPLATE",
nargs=2,
multiple=True,
help="Search for photos where TEMPLATE matches regular expression REGEX. "
"For example, to find photos in an album that begins with 'Beach': '--regex \"^Beach\" \"{album}\"'. "
"You may specify more than one regular expression match by repeating '--regex' with different arguments.",
),
o(
"--selected",
is_flag=True,
help="Filter for photos that are currently selected in Photos.",
),
o(
"--exif",
metavar="EXIF_TAG VALUE",
nargs=2,
multiple=True,
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
"To use --exif, exiftool must be installed and in the path.",
),
o(
"--query-eval",
metavar="CRITERIA",
multiple=True,
help="Evaluate CRITERIA to filter photos. "
"CRITERIA will be evaluated in context of the following python list comprehension: "
"`photos = [photo for photo in photos if CRITERIA]` "
"where photo represents a PhotoInfo object. "
"For example: `--query-eval photo.favorite` returns all photos that have been "
"favorited and is equivalent to --favorite. "
"You may specify more than one CRITERIA by using --query-eval multiple times. "
"CRITERIA must be a valid python expression. "
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
),
o(
"--query-function",
metavar="filename.py::function",
multiple=True,
type=FunctionCall(),
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
+ "file you've created and function is the name of the function in the python file you want to call. "
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
+ "You may use more than one function by repeating the --query-function option with a different value. "
+ "Your query function will be called after all other query options have been evaluated. "
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
),
]
for o in options[::-1]:
f = o(f)
return f
def DEBUG_OPTIONS(f):
o = click.option
options = [
o(
"--debug",
is_flag=True,
help="Enable debug output.",
hidden=OSXPHOTOS_HIDDEN,
),
o(
"--watch",
metavar="FUNCTION_PATH",
multiple=True,
help="Watch function calls. For example, to watch all calls to FileUtil.copy: "
"'--watch osxphotos.fileutil.FileUtil.copy'. More than one --watch option can be specified.",
hidden=OSXPHOTOS_HIDDEN,
),
o(
"--breakpoint",
metavar="FUNCTION_PATH",
multiple=True,
help="Add breakpoint to function calls. For example, to add breakpoint to FileUtil.copy: "
"'--breakpoint osxphotos.fileutil.FileUtil.copy'. More than one --breakpoint option can be specified.",
hidden=OSXPHOTOS_HIDDEN,
),
]
for o in options[::-1]:
f = o(f)
return f
THEME_OPTION = click.option(
"--theme",
metavar="THEME",
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
help="Specify the color theme to use for --verbose output. "
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
)
def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored.
Whitespace is stripped.
Arguments:
filename: file name of the file containing UUIDs
Returns:
list of UUIDs or empty list of no UUIDs in file
Raises:
FileNotFoundError if file does not exist
"""
if not pathlib.Path(filename).is_file():
raise FileNotFoundError(f"Could not find file {filename}")
uuid = []
with open(filename, "r") as uuid_file:
for line in uuid_file:
line = line.strip()
if len(line) and line[0] != "#":
uuid.append(line)
return uuid
def get_config_dir() -> pathlib.Path:
"""Get the directory where config files are stored."""
config_dir = pathlib.Path.home() / ".config" / APP_NAME
if not config_dir.is_dir():
config_dir.mkdir(parents=True)
return config_dir

19
osxphotos/cli/darkmode.py Normal file
View File

@@ -0,0 +1,19 @@
"""Detect dark mode on MacOS >= 10.14"""
import objc
import Foundation
def theme():
with objc.autorelease_pool():
user_defaults = Foundation.NSUserDefaults.standardUserDefaults()
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
return "dark" if system_theme == "Dark" else "light"
def is_dark_mode():
return theme() == "dark"
def is_light_mode():
return theme() == "light"

View File

@@ -0,0 +1,97 @@
"""debug-dump command for osxphotos CLI"""
import pprint
import time
import click
from rich import print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
from .list import _list_libraries
from .verbose import verbose_print
@click.command(hidden=OSXPHOTOS_HIDDEN)
@DB_OPTION
@DB_ARGUMENT
@click.option(
"--dump",
metavar="ATTR",
help="Name of PhotosDB attribute to print; "
+ "can also use albums, persons, keywords, photos to dump related attributes.",
multiple=True,
)
@click.option(
"--uuid",
metavar="UUID",
help="Use with '--dump photos' to dump only certain UUIDs. "
"May be repeated to include multiple UUIDs.",
multiple=True,
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@click.pass_obj
@click.pass_context
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
"""Print out debug info"""
verbose_ = verbose_print(verbose, rich=True)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
start_t = time.perf_counter()
print(f"Opening database: {db}")
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
stop_t = time.perf_counter()
print(f"Done; took {(stop_t-start_t):.2f} seconds")
for attr in dump:
if attr == "albums":
print("_dbalbums_album:")
pprint.pprint(photosdb._dbalbums_album)
print("_dbalbums_uuid:")
pprint.pprint(photosdb._dbalbums_uuid)
print("_dbalbum_details:")
pprint.pprint(photosdb._dbalbum_details)
print("_dbalbum_folders:")
pprint.pprint(photosdb._dbalbum_folders)
print("_dbfolder_details:")
pprint.pprint(photosdb._dbfolder_details)
elif attr == "keywords":
print("_dbkeywords_keyword:")
pprint.pprint(photosdb._dbkeywords_keyword)
print("_dbkeywords_uuid:")
pprint.pprint(photosdb._dbkeywords_uuid)
elif attr == "persons":
print("_dbfaces_uuid:")
pprint.pprint(photosdb._dbfaces_uuid)
print("_dbfaces_pk:")
pprint.pprint(photosdb._dbfaces_pk)
print("_dbpersons_pk:")
pprint.pprint(photosdb._dbpersons_pk)
print("_dbpersons_fullname:")
pprint.pprint(photosdb._dbpersons_fullname)
elif attr == "photos":
if uuid:
for uuid_ in uuid:
print(f"_dbphotos['{uuid_}']:")
try:
pprint.pprint(photosdb._dbphotos[uuid_])
except KeyError:
print(f"Did not find uuid {uuid_} in _dbphotos")
else:
print("_dbphotos:")
pprint.pprint(photosdb._dbphotos)
else:
try:
val = getattr(photosdb, attr)
print(f"{attr}:")
pprint.pprint(val)
except Exception:
print(f"Did not find attribute {attr} in PhotosDB")

64
osxphotos/cli/docs.py Normal file
View File

@@ -0,0 +1,64 @@
"""docs command for osxphotos CLI """
import pathlib
import shutil
from typing import Optional
import click
from osxphotos._version import __version__
from .common import get_config_dir
@click.command()
@click.pass_obj
@click.pass_context
def docs(ctx, cli_obj):
"""Open osxphotos documentation in your browser."""
docs_dir = get_config_dir() / "docs"
if not docs_dir.exists():
docs_dir.mkdir(parents=True)
docs_version = get_docs_version(docs_dir)
if not docs_version or docs_version != __version__:
click.echo(f"Copying docs for osxphotos version {__version__}")
shutil.rmtree(str(docs_dir), ignore_errors=True)
copy_docs(docs_dir)
set_docs_version(docs_dir, __version__)
index = docs_dir / "index.html"
click.echo(f"Opening {index}")
click.launch(str(index))
def get_docs_version(docs_dir: pathlib.Path) -> Optional[str]:
"""Get the version of the docs directory"""
if not docs_dir.exists():
return None
version_file = docs_dir / ".version"
if not version_file.exists():
return None
with version_file.open() as f:
return f.read().strip()
def copy_docs(docs_dir: pathlib.Path):
"""Copy the latest docs to the docs directory"""
# there must be a better way to do this
# docs are in osxphotos/docs and this file is in osxphotos/cli
src_dir = pathlib.Path(__file__).parent.parent / "docs"
shutil.copytree(str(src_dir), str(docs_dir))
def set_docs_version(docs_dir: pathlib.Path, version: str):
"""Set the version of the docs directory"""
version_file = docs_dir / ".version"
if version_file.exists():
version_file.unlink()
with version_file.open("w") as f:
f.write(version)

44
osxphotos/cli/dump.py Normal file
View File

@@ -0,0 +1,44 @@
"""dump command for osxphotos CLI """
import click
import osxphotos
from osxphotos.queryoptions import QueryOptions
from .common import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, JSON_OPTION, get_photos_db
from .list import _list_libraries
from .print_photo_info import print_photo_info
@click.command()
@DB_OPTION
@JSON_OPTION
@DELETED_OPTIONS
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library):
"""Print list of all photos & associated info from the Photos library."""
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
# check exclusive options
if deleted and deleted_only:
click.echo("Incompatible dump options", err=True)
click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True)
return
photosdb = osxphotos.PhotosDB(dbfile=db)
if deleted or deleted_only:
photos = photosdb.photos(movies=True, intrash=True)
else:
photos = []
if not deleted_only:
photos += photosdb.photos(movies=True)
print_photo_info(photos, json_ or cli_obj.json)

File diff suppressed because it is too large Load Diff

252
osxphotos/cli/exportdb.py Normal file
View File

@@ -0,0 +1,252 @@
"""exportdb command for osxphotos CLI"""
import pathlib
import sys
import click
from rich import print
from osxphotos._constants import OSXPHOTOS_EXPORT_DB
from osxphotos._version import __version__
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
from osxphotos.export_db_utils import (
export_db_check_signatures,
export_db_get_last_run,
export_db_get_version,
export_db_save_config_to_file,
export_db_touch_files,
export_db_update_signatures,
export_db_vacuum,
)
from .common import OSXPHOTOS_HIDDEN
from .verbose import verbose_print
@click.command(name="exportdb", hidden=OSXPHOTOS_HIDDEN)
@click.option("--version", is_flag=True, help="Print export database version and exit.")
@click.option("--vacuum", is_flag=True, help="Run VACUUM to defragment the database.")
@click.option(
"--check-signatures",
is_flag=True,
help="Check signatures for all exported photos in the database to find signatures that don't match.",
)
@click.option(
"--update-signatures",
is_flag=True,
help="Update signatures for all exported photos in the database to match on-disk signatures.",
)
@click.option(
"--touch-file",
is_flag=True,
help="Touch files on disk to match created date in Photos library and update export database signatures",
)
@click.option(
"--last-run",
is_flag=True,
help="Show last run osxphotos commands used with this database.",
)
@click.option(
"--save-config",
metavar="CONFIG_FILE",
help="Save last run configuration to TOML file for use by --load-config.",
)
@click.option(
"--info",
metavar="FILE_PATH",
nargs=1,
help="Print information about FILE_PATH contained in the database.",
)
@click.option(
"--migrate",
is_flag=True,
help="Migrate (if needed) export database to current version.",
)
@click.option(
"--sql",
metavar="SQL_STATEMENT",
help="Execute SQL_STATEMENT against export database and print results.",
)
@click.option(
"--export-dir",
help="Optional path to export directory (if not parent of export database).",
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
@click.option(
"--dry-run",
is_flag=True,
help="Run in dry-run mode (don't actually update files), e.g. for use with --update-signatures.",
)
@click.argument("export_db", metavar="EXPORT_DATABASE", type=click.Path(exists=True))
def exportdb(
version,
vacuum,
check_signatures,
update_signatures,
touch_file,
last_run,
save_config,
info,
migrate,
sql,
export_dir,
verbose,
dry_run,
export_db,
):
"""Utilities for working with the osxphotos export database"""
verbose_ = verbose_print(verbose, rich=True)
export_db = pathlib.Path(export_db)
if export_db.is_dir():
# assume it's the export folder
export_db = export_db / OSXPHOTOS_EXPORT_DB
if not export_db.is_file():
print(
f"[red]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/red]"
)
sys.exit(1)
export_dir = export_dir or export_db.parent
sub_commands = [
version,
check_signatures,
update_signatures,
touch_file,
last_run,
bool(save_config),
bool(info),
migrate,
bool(sql),
]
if sum(sub_commands) > 1:
print("[red]Only a single sub-command may be specified at a time[/red]")
sys.exit(1)
if version:
try:
osxphotos_ver, export_db_ver = export_db_get_version(export_db)
except Exception as e:
print(f"[red]Error: could not read version from {export_db}: {e}[/red]")
sys.exit(1)
else:
print(
f"osxphotos version: {osxphotos_ver}, export database version: {export_db_ver}"
)
sys.exit(0)
if vacuum:
try:
start_size = pathlib.Path(export_db).stat().st_size
export_db_vacuum(export_db)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Vacuumed {export_db}! {start_size} bytes -> {pathlib.Path(export_db).stat().st_size} bytes"
)
sys.exit(0)
if update_signatures:
try:
updated, skipped = export_db_update_signatures(
export_db, export_dir, verbose_, dry_run
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"Done. Updated {updated} files, skipped {skipped} files.")
sys.exit(0)
if last_run:
try:
last_run_info = export_db_get_last_run(export_db)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"last run at {last_run_info[0]}:")
print(f"osxphotos {last_run_info[1]}")
sys.exit(0)
if save_config:
try:
export_db_save_config_to_file(export_db, save_config)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"Saved configuration to {save_config}")
sys.exit(0)
if check_signatures:
try:
matched, notmatched, skipped = export_db_check_signatures(
export_db, export_dir, verbose_=verbose_
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Done. Found {matched} matching signatures and {notmatched} signatures that don't match. Skipped {skipped} missing files."
)
sys.exit(0)
if touch_file:
try:
touched, not_touched, skipped = export_db_touch_files(
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Done. Touched {touched} files, skipped {not_touched} up to date files, skipped {skipped} missing files."
)
sys.exit(0)
if info:
exportdb = ExportDB(export_db, export_dir)
try:
info_rec = exportdb.get_file_record(info)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
if info_rec:
print(info_rec.asdict())
else:
print(f"[red]File '{info}' not found in export database[/red]")
sys.exit(0)
if migrate:
exportdb = ExportDB(export_db, export_dir)
if upgraded := exportdb.was_upgraded:
print(
f"Migrated export database {export_db} from version {upgraded[0]} to {upgraded[1]}"
)
else:
print(
f"Export database {export_db} is already at latest version {OSXPHOTOS_EXPORTDB_VERSION}"
)
sys.exit(0)
if sql:
exportdb = ExportDB(export_db, export_dir)
try:
c = exportdb._conn.cursor()
results = c.execute(sql)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
for row in results:
print(row)
sys.exit(0)

57
osxphotos/cli/grep.py Normal file
View File

@@ -0,0 +1,57 @@
"""grep command for osxphotos CLI """
import pathlib
import click
from rich import print
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
from osxphotos.sqlgrep import sqlgrep
from .common import DB_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
@click.command(name="grep", hidden=OSXPHOTOS_HIDDEN)
@DB_OPTION
@click.pass_obj
@click.pass_context
@click.option(
"--ignore-case",
"-i",
required=False,
is_flag=True,
default=False,
help="Ignore case when searching (default is case-sensitive).",
)
@click.option(
"--print-filename",
"-p",
required=False,
is_flag=True,
default=False,
help="Print name of database file when printing results.",
)
@click.argument("pattern", metavar="PATTERN", required=True)
def grep(ctx, cli_obj, db, ignore_case, print_filename, pattern):
"""Search for PATTERN in the Photos sqlite database file"""
db = db or get_photos_db()
db = pathlib.Path(db)
if db.is_file():
# if passed the actual database, really want the parent of the database directory
db = db.parent.parent
photos_ver = get_photos_library_version(str(db))
if photos_ver < 5:
db_file = db / "database" / "photos.db"
else:
db_file = db / "database" / "Photos.sqlite"
if not db_file.is_file():
click.secho(f"Could not find database file {db_file}", fg="red")
ctx.exit(2)
db_file = str(db_file)
for table, column, row_id, value in sqlgrep(
db_file, pattern, ignore_case, print_filename, rich_markup=True
):
print(", ".join([table, column, row_id, value]))

View File

@@ -1,27 +1,209 @@
"""Help text helper class for osxphotos CLI """
import io
import pathlib
import inspect
import re
import typing as t
import click
import osxmetadata
from rich.console import Console
from rich.markdown import Markdown
from ._constants import (
from osxphotos._constants import (
EXTENDED_ATTRIBUTE_NAMES,
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
OSXPHOTOS_EXPORT_DB,
POST_COMMAND_CATEGORIES,
)
from .phototemplate import (
from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
TEMPLATE_SUBSTITUTIONS_PATHLIB,
get_template_help,
)
from .click_rich_echo import rich_echo_via_pager
from .color_themes import get_theme
from .common import OSXPHOTOS_HIDDEN
HELP_WIDTH = 110
HIGHLIGHT_COLOR = "yellow"
__all__ = [
"ExportCommand",
"template_help",
"rich_text",
"strip_md_header_and_links",
"strip_md_links",
"strip_html_comments",
"help",
"get_help_msg",
]
def get_help_msg(command):
"""get help message for a Click command"""
with click.Context(command) as ctx:
return command.get_help(ctx)
@click.command()
@click.option(
"--width",
default=HELP_WIDTH,
help="Width of help text",
hidden=OSXPHOTOS_HIDDEN,
)
@click.argument("topic", default=None, required=False, nargs=1)
@click.argument("subtopic", default=None, required=False, nargs=1)
@click.pass_context
def help(ctx, topic, subtopic, width, **kw):
"""Print help; for help on commands: help <command>."""
if topic is None:
click.echo(ctx.parent.get_help())
return
global HELP_WIDTH
HELP_WIDTH = width
wrap_text_original = click.formatting.wrap_text
def wrap_text(
text: str,
width: int = HELP_WIDTH,
initial_indent: str = "",
subsequent_indent: str = "",
preserve_paragraphs: bool = False,
) -> str:
return wrap_text_original(
text,
width=width,
initial_indent=initial_indent,
subsequent_indent=subsequent_indent,
preserve_paragraphs=preserve_paragraphs,
)
click.formatting.wrap_text = wrap_text
click.wrap_text = wrap_text
if subtopic:
cmd = ctx.obj.group.commands[topic]
rich_echo_via_pager(
get_subtopic_help(cmd, ctx, subtopic),
theme=get_theme(),
width=HELP_WIDTH,
)
return
if topic in ctx.obj.group.commands:
ctx.info_name = topic
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
return
# didn't find any valid help topics
click.echo(f"Invalid command: {topic}", err=True)
click.echo(ctx.parent.get_help())
def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
"""Get help for a command including only options that match a subtopic"""
# set ctx.info_name or click prints the wrong usage str (usage for help instead of cmd)
ctx.info_name = cmd.name
usage_str = cmd.get_help(ctx)
usage_str = usage_str.partition("\n")[0]
info = cmd.to_info_dict(ctx)
help_str = info.get("help", "")
options = get_matching_options(cmd, ctx, subtopic)
# format help text and options
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write(usage_str)
formatter.write_paragraph()
format_help_text(help_str, formatter)
formatter.write_paragraph()
if options:
option_str = format_options_help(options, ctx, highlight=subtopic)
formatter.write(f"Options that match '[highlight]{subtopic}[/highlight]':\n")
formatter.write_paragraph()
formatter.write(option_str)
else:
formatter.write(f"No options match '[highlight]{subtopic}[/highlight]'")
return formatter.getvalue()
def get_matching_options(
command: click.Command, ctx: click.Context, topic: str
) -> t.List:
"""Get matching options for a command that contain a topic
Args:
command: click.Command
ctx: click.Context
topic: str, topic to match
Returns:
list of matching click.Option objects
"""
options = []
topic = topic.lower()
for option in command.params:
help_record = option.get_help_record(ctx)
if help_record and (topic in help_record[0] or topic in help_record[1]):
options.append(option)
return options
def format_options_help(
options: t.List[click.Option], ctx: click.Context, highlight: t.Optional[str] = None
) -> str:
"""Format options help for display
Args:
options: list of click.Option objects
ctx: click.Context
highlight: str, if set, add rich highlighting to options that match highlight str
Returns:
str with formatted help
"""
formatter = click.HelpFormatter(width=HELP_WIDTH)
opt_help = [opt.get_help_record(ctx) for opt in options]
if highlight:
# convert list of tuples to list of lists
opt_help = [list(opt) for opt in opt_help]
for record in opt_help:
record[0] = re.sub(
f"({highlight})",
"[highlight]\\1[/highlight]",
record[0],
re.IGNORECASE,
)
record[1] = re.sub(
f"({highlight})",
"[highlight]\\1[/highlight]",
record[1],
re.IGNORECASE,
)
# convert back to list of tuples as that's what write_dl expects
opt_help = [tuple(opt) for opt in opt_help]
formatter.write_dl(opt_help)
return formatter.getvalue()
def format_help_text(text: str, formatter: click.HelpFormatter):
text = inspect.cleandoc(text).partition("\f")[0]
formatter.write_paragraph()
with formatter.indentation():
formatter.write_text(text)
# TODO: The following help text could probably be done as mako template
class ExportCommand(click.Command):
@@ -29,11 +211,9 @@ class ExportCommand(click.Command):
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 = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n")
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
formatter.write("\n")
formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level "
@@ -68,9 +248,9 @@ class ExportCommand(click.Command):
+ "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("\n")
formatter.write(
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write_text(
@@ -91,25 +271,33 @@ The following attributes may be used with '--xattr-template':
"""
)
formatter.write_dl(
[
attr_tuples = [
(
rich_text("[bold]Attribute[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
),
*[
(
attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
)
for attr in EXTENDED_ATTRIBUTE_NAMES
]
)
],
]
formatter.write_dl(attr_tuples)
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("\n")
formatter.write(
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
rich_text("## Templating System", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write(template_help(width=formatter.width))
help_text += formatter.getvalue()
help_text += template_help(width=formatter.width)
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n")
formatter.write_text(
"With the --directory and --filename options you may specify a template for the "
@@ -137,12 +325,15 @@ The following attributes may be used with '--xattr-template':
)
formatter.write("\n")
formatter.write(
rich_text(
"[bold]** Template Substitutions **[/bold]", width=formatter.width
)
rich_text("## Template Substitutions", width=formatter.width, markdown=True)
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples = [
(
rich_text("[bold]Substitution[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
)
]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
formatter.write_dl(templ_tuples)
@@ -157,7 +348,12 @@ The following attributes may be used with '--xattr-template':
+ "2019/Vacation, 2019/Family"
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples = [
(
rich_text("[bold]Substitution[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
)
]
templ_tuples.extend(
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
)
@@ -195,10 +391,11 @@ The following attributes may be used with '--xattr-template':
formatter.write_dl(templ_tuples)
formatter.write("\n\n")
formatter.write("\n")
formatter.write(
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
rich_text("## Post Command", width=formatter.width, markdown=True)
)
formatter.write("\n")
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. "
@@ -207,7 +404,7 @@ The following attributes may be used with '--xattr-template':
+ "The following categories are available: "
)
formatter.write("\n")
templ_tuples = [("Catgory", "Description")]
templ_tuples = [("Category", "Description")]
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
formatter.write_dl(templ_tuples)
formatter.write("\n")
@@ -224,13 +421,13 @@ The following attributes may be used with '--xattr-template':
)
formatter.write("\n")
formatter.write(
'--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
'--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
)
formatter.write("\n\n")
formatter.write_text(
"In the above command, the 'shell_quote' filter is used to ensure '{filepath.name}' is properly quoted "
"In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is properly quoted "
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
"If '{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
"If '{filepath}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
"thus renders to: "
)
formatter.write("\n")
@@ -241,10 +438,11 @@ The following attributes may be used with '--xattr-template':
+ "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("\n")
formatter.write(
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
rich_text("## Post Function", width=formatter.width, markdown=True)
)
formatter.write("\n")
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 "
@@ -262,36 +460,19 @@ The following attributes may be used with '--xattr-template':
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
console = Console(force_terminal=True, width=width)
with console.capture() as capture:
console.print(Markdown(template_help_md))
return capture.get()
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):
def rich_text(text, width=78, markdown=False):
"""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
console = Console(force_terminal=True, width=width)
with console.capture() as capture:
console.print(Markdown(text) if markdown else text, end="")
return capture.get()
def strip_md_header_and_links(md):
@@ -337,12 +518,3 @@ def strip_md_links(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

72
osxphotos/cli/info.py Normal file
View File

@@ -0,0 +1,72 @@
"""info command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def info(ctx, cli_obj, db, json_, photos_library):
"""Print out descriptive info of the Photos library database."""
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["info"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
info = {"database_path": photosdb.db_path, "database_version": photosdb.db_version}
photos = photosdb.photos(movies=False)
not_shared_photos = [p for p in photos if not p.shared]
info["photo_count"] = len(not_shared_photos)
hidden = [p for p in photos if p.hidden]
info["hidden_photo_count"] = len(hidden)
movies = photosdb.photos(images=False, movies=True)
not_shared_movies = [p for p in movies if not p.shared]
info["movie_count"] = len(not_shared_movies)
if photosdb.db_version > _PHOTOS_4_VERSION:
shared_photos = [p for p in photos if p.shared]
info["shared_photo_count"] = len(shared_photos)
shared_movies = [p for p in movies if p.shared]
info["shared_movie_count"] = len(shared_movies)
keywords = photosdb.keywords_as_dict
info["keywords_count"] = len(keywords)
info["keywords"] = keywords
albums = photosdb.albums_as_dict
info["albums_count"] = len(albums)
info["albums"] = albums
if photosdb.db_version > _PHOTOS_4_VERSION:
albums_shared = photosdb.albums_shared_as_dict
info["shared_albums_count"] = len(albums_shared)
info["shared_albums"] = albums_shared
persons = photosdb.persons_as_dict
info["persons_count"] = len(persons)
info["persons"] = persons
if cli_obj.json or json_:
click.echo(json.dumps(info, ensure_ascii=False))
else:
click.echo(yaml.dump(info, sort_keys=False, allow_unicode=True))

View File

@@ -0,0 +1,37 @@
"""install/uninstall/run commands for osxphotos CLI"""
import sys
from runpy import run_module, run_path
import click
@click.command()
@click.argument("packages", nargs=-1, required=True)
@click.option(
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
)
def install(packages, upgrade):
"""Install Python packages into the same environment as osxphotos"""
args = ["pip", "install"]
if upgrade:
args += ["--upgrade"]
args += list(packages)
sys.argv = args
run_module("pip", run_name="__main__")
@click.command()
@click.argument("packages", nargs=-1, required=True)
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
def uninstall(packages, yes):
"""Uninstall Python packages from the osxphotos environment"""
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
run_module("pip", run_name="__main__")
@click.command(name="run")
@click.argument("python_file", nargs=1, type=click.Path(exists=True))
def run(python_file):
"""Run a python file using same environment as osxphotos"""
run_path(python_file, run_name="__main__")

37
osxphotos/cli/keywords.py Normal file
View File

@@ -0,0 +1,37 @@
"""keywords command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def keywords(ctx, cli_obj, db, json_, photos_library):
"""Print out keywords found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["keywords"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
keywords = {"keywords": photosdb.keywords_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(keywords, ensure_ascii=False))
else:
click.echo(yaml.dump(keywords, sort_keys=False, allow_unicode=True))

37
osxphotos/cli/labels.py Normal file
View File

@@ -0,0 +1,37 @@
"""labels command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def labels(ctx, cli_obj, db, json_, photos_library):
"""Print out image classification labels found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["labels"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
labels = {"labels": photosdb.labels_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(labels, ensure_ascii=False))
else:
click.echo(yaml.dump(labels, sort_keys=False, allow_unicode=True))

57
osxphotos/cli/list.py Normal file
View File

@@ -0,0 +1,57 @@
"""list command for osxphotos CLI"""
import json
import click
import osxphotos
from .common import JSON_OPTION
@click.command(name="list")
@JSON_OPTION
@click.pass_obj
@click.pass_context
def list_libraries(ctx, cli_obj, json_):
"""Print list of Photos libraries found on the system."""
# implemented in _list_libraries so it can be called by other CLI functions
# without errors due to passing ctx and cli_obj
_list_libraries(json_=json_ or cli_obj.json, error=False)
def _list_libraries(json_=False, error=True):
"""Print list of Photos libraries found on the system.
If json_ == True, print output as JSON (default = False)"""
photo_libs = osxphotos.utils.list_photo_libraries()
sys_lib = osxphotos.utils.get_system_library_path()
last_lib = osxphotos.utils.get_last_library_path()
if json_:
libs = {
"photo_libraries": photo_libs,
"system_library": sys_lib,
"last_library": last_lib,
}
click.echo(json.dumps(libs, ensure_ascii=False))
else:
last_lib_flag = sys_lib_flag = False
for lib in photo_libs:
if lib == sys_lib:
click.echo(f"(*)\t{lib}", err=error)
sys_lib_flag = True
elif lib == last_lib:
click.echo(f"(#)\t{lib}", err=error)
last_lib_flag = True
else:
click.echo(f"\t{lib}", err=error)
if sys_lib_flag or last_lib_flag:
click.echo("\n", err=error)
if sys_lib_flag:
click.echo("(*)\tSystem Photos Library", err=error)
if last_lib_flag:
click.echo("(#)\tLast opened Photos Library", err=error)

View File

@@ -0,0 +1,131 @@
"""Click parameter types for osxphotos CLI"""
import datetime
import os
import pathlib
import bitmath
import click
from osxphotos.export_db_utils import export_db_get_version
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.utils import expand_and_validate_filepath, load_function
__all__ = [
"BitMathSize",
"DateTimeISO8601",
"ExportDBType",
"FunctionCall",
"TimeISO8601",
"TemplateString",
]
class DateTimeISO8601(click.ParamType):
name = "DATETIME"
def convert(self, value, param, ctx):
try:
return datetime.datetime.fromisoformat(value)
except Exception:
self.fail(
f"Invalid datetime format {value}. "
"Valid format: YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]"
)
class BitMathSize(click.ParamType):
name = "BITMATH"
def convert(self, value, param, ctx):
try:
value = bitmath.parse_string(value)
except ValueError:
# no units specified
try:
value = int(value)
value = bitmath.Byte(value)
except ValueError as e:
self.fail(
f"{value} must be specified as bytes or using SI/NIST units. "
+ "For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'."
)
return value
class TimeISO8601(click.ParamType):
name = "TIME"
def convert(self, value, param, ctx):
try:
return datetime.time.fromisoformat(value).replace(tzinfo=None)
except Exception:
self.fail(
f"Invalid time format {value}. "
"Valid format: HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] "
"however, note that timezone will be ignored."
)
class FunctionCall(click.ParamType):
name = "FUNCTION"
def convert(self, value, param, ctx):
if "::" not in value:
self.fail(
f"Could not parse function name from '{value}'. "
"Valid format filename.py::function"
)
filename, funcname = value.split("::")
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
self.fail(f"'{filename}' does not appear to be a file")
try:
function = load_function(filename_validated, funcname)
except Exception as e:
self.fail(f"Could not load function {funcname} from {filename_validated}")
return (function, value)
class ExportDBType(click.ParamType):
name = "EXPORTDB"
def convert(self, value, param, ctx):
try:
export_db_name = pathlib.Path(value)
if export_db_name.is_dir():
raise click.BadParameter(f"{value} is a directory")
if export_db_name.is_file():
# verify it's actually an osxphotos export_db
# export_db_get_version will raise an error if it's not valid
osxphotos_ver, export_db_ver = export_db_get_version(value)
return value
except Exception:
self.fail(f"{value} exists but is not a valid osxphotos export database. ")
class TemplateString(click.ParamType):
"""Validate an osxphotos template language (OTL) template string"""
name = "OTL_TEMPLATE"
def convert(self, value, param, ctx):
try:
cwd = os.getcwd()
_, unmatched = PhotoTemplate(photo=PhotoInfoNone()).render(
value,
options=RenderOptions(export_dir=cwd, dest_path=cwd, filepath=cwd),
)
if unmatched:
self.fail(f"Template '{value}' contains unknown field(s): {unmatched}")
return value
except ValueError as e:
self.fail(e)

36
osxphotos/cli/persons.py Normal file
View File

@@ -0,0 +1,36 @@
"""persons command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def persons(ctx, cli_obj, db, json_, photos_library):
"""Print out persons (faces) found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["persons"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
persons = {"persons": photosdb.persons_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(persons, ensure_ascii=False))
else:
click.echo(yaml.dump(persons, sort_keys=False, allow_unicode=True))

62
osxphotos/cli/places.py Normal file
View File

@@ -0,0 +1,62 @@
"""places command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def places(ctx, cli_obj, db, json_, photos_library):
"""Print out places found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["places"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
place_names = {}
for photo in photosdb.photos(movies=True):
if photo.place:
try:
place_names[photo.place.name] += 1
except Exception:
place_names[photo.place.name] = 1
else:
try:
place_names[_UNKNOWN_PLACE] += 1
except Exception:
place_names[_UNKNOWN_PLACE] = 1
# sort by place count
places = {
"places": {
name: place_names[name]
for name in sorted(
place_names.keys(), key=lambda key: place_names[key], reverse=True
)
}
}
# below needed for to make CliRunner work for testing
cli_json = cli_obj.json if cli_obj is not None else None
if json_ or cli_json:
click.echo(json.dumps(places, ensure_ascii=False))
else:
click.echo(yaml.dump(places, sort_keys=False, allow_unicode=True))

View File

@@ -0,0 +1,112 @@
"""print_photo_info function to print PhotoInfo objects"""
import csv
import sys
from typing import Callable, List
from osxphotos.photoinfo import PhotoInfo
def print_photo_info(
photos: List[PhotoInfo], json: bool = False, print_func: Callable = print
):
dump = []
if json:
dump.extend(p.json() for p in photos)
print_func(f"[{', '.join(dump)}]")
else:
# dump as CSV
csv_writer = csv.writer(
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
)
# add headers
dump.append(
[
"uuid",
"filename",
"original_filename",
"date",
"description",
"title",
"keywords",
"albums",
"persons",
"path",
"ismissing",
"hasadjustments",
"external_edit",
"favorite",
"hidden",
"shared",
"latitude",
"longitude",
"path_edited",
"isphoto",
"ismovie",
"uti",
"burst",
"live_photo",
"path_live_photo",
"iscloudasset",
"incloud",
"date_modified",
"portrait",
"screenshot",
"slow_mo",
"time_lapse",
"hdr",
"selfie",
"panorama",
"has_raw",
"uti_raw",
"path_raw",
"intrash",
]
)
for p in photos:
date_modified_iso = p.date_modified.isoformat() if p.date_modified else None
dump.append(
[
p.uuid,
p.filename,
p.original_filename,
p.date.isoformat(),
p.description,
p.title,
", ".join(p.keywords),
", ".join(p.albums),
", ".join(p.persons),
p.path,
p.ismissing,
p.hasadjustments,
p.external_edit,
p.favorite,
p.hidden,
p.shared,
p._latitude,
p._longitude,
p.path_edited,
p.isphoto,
p.ismovie,
p.uti,
p.burst,
p.live_photo,
p.path_live_photo,
p.iscloudasset,
p.incloud,
date_modified_iso,
p.portrait,
p.screenshot,
p.slow_mo,
p.time_lapse,
p.hdr,
p.selfie,
p.panorama,
p.has_raw,
p.uti_raw,
p.path_raw,
p.intrash,
]
)
for row in dump:
csv_writer.writerow(row)

354
osxphotos/cli/query.py Normal file
View File

@@ -0,0 +1,354 @@
"""query command for osxphotos CLI"""
import click
import osxphotos
from osxphotos.debug import set_debug
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.queryoptions import QueryOptions
from .common import (
CLI_COLOR_ERROR,
CLI_COLOR_WARNING,
DB_ARGUMENT,
DB_OPTION,
DELETED_OPTIONS,
JSON_OPTION,
OSXPHOTOS_HIDDEN,
QUERY_OPTIONS,
get_photos_db,
load_uuid_from_file,
)
from .list import _list_libraries
from .print_photo_info import print_photo_info
@click.command()
@DB_OPTION
@JSON_OPTION
@QUERY_OPTIONS
@DELETED_OPTIONS
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
@click.option(
"--not-missing",
is_flag=True,
help="Search for photos present on disk (e.g. not missing).",
)
@click.option(
"--cloudasset",
is_flag=True,
help="Search for photos that are part of an iCloud library",
)
@click.option(
"--not-cloudasset",
is_flag=True,
help="Search for photos that are not part of an iCloud library",
)
@click.option(
"--incloud",
is_flag=True,
help="Search for photos that are in iCloud (have been synched)",
)
@click.option(
"--not-incloud",
is_flag=True,
help="Search for photos that are not in iCloud (have not been synched)",
)
@click.option(
"--add-to-album",
metavar="ALBUM",
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
"if it doesn't exist. All photos in the query results will be added to this album. "
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
"This feature is currently experimental. I don't know how well it will work on large query sets.",
)
@click.option(
"--debug", required=False, is_flag=True, default=False, hidden=OSXPHOTOS_HIDDEN
)
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def query(
ctx,
cli_obj,
db,
photos_library,
keyword,
person,
album,
folder,
name,
uuid,
uuid_from_file,
title,
no_title,
description,
no_description,
ignore_case,
json_,
edited,
external_edit,
favorite,
not_favorite,
hidden,
not_hidden,
missing,
not_missing,
shared,
not_shared,
only_movies,
only_photos,
uti,
burst,
not_burst,
live,
not_live,
cloudasset,
not_cloudasset,
incloud,
not_incloud,
from_date,
to_date,
from_time,
to_time,
portrait,
not_portrait,
screenshot,
not_screenshot,
slow_mo,
not_slow_mo,
time_lapse,
not_time_lapse,
hdr,
not_hdr,
selfie,
not_selfie,
panorama,
not_panorama,
has_raw,
place,
no_place,
location,
no_location,
label,
deleted,
deleted_only,
has_comment,
no_comment,
has_likes,
no_likes,
is_reference,
in_album,
not_in_album,
duplicate,
min_size,
max_size,
regex,
selected,
exif,
query_eval,
query_function,
add_to_album,
debug, # handled in cli/__init__.py
):
"""Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND"
(e.g. search for photos matching all options).
"""
# if no query terms, show help and return
# sanity check input args
nonexclusive = [
keyword,
person,
album,
folder,
name,
uuid,
uuid_from_file,
edited,
external_edit,
uti,
has_raw,
from_date,
to_date,
from_time,
to_time,
label,
is_reference,
query_eval,
query_function,
min_size,
max_size,
regex,
selected,
exif,
duplicate,
]
exclusive = [
(favorite, not_favorite),
(hidden, not_hidden),
(missing, not_missing),
(any(title), no_title),
(any(description), no_description),
(only_photos, only_movies),
(burst, not_burst),
(live, not_live),
(cloudasset, not_cloudasset),
(incloud, not_incloud),
(portrait, not_portrait),
(screenshot, not_screenshot),
(slow_mo, not_slow_mo),
(time_lapse, not_time_lapse),
(hdr, not_hdr),
(selfie, not_selfie),
(panorama, not_panorama),
(any(place), no_place),
(deleted, deleted_only),
(shared, not_shared),
(has_comment, no_comment),
(has_likes, no_likes),
(in_album, not_in_album),
(location, no_location),
]
# print help if no non-exclusive term or a double exclusive term is given
if any(all(bb) for bb in exclusive) or not any(
nonexclusive + [b ^ n for b, n in exclusive]
):
click.echo("Incompatible query options", err=True)
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
return
# actually have something to query
# default searches for everything
photos = True
movies = True
if only_movies:
photos = False
if only_photos:
movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
if uuid_from_file:
uuid_list = list(uuid) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(uuid_from_file))
uuid = tuple(uuid_list)
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
query_options = QueryOptions(
keyword=keyword,
person=person,
album=album,
folder=folder,
uuid=uuid,
title=title,
no_title=no_title,
description=description,
no_description=no_description,
ignore_case=ignore_case,
edited=edited,
external_edit=external_edit,
favorite=favorite,
not_favorite=not_favorite,
hidden=hidden,
not_hidden=not_hidden,
missing=missing,
not_missing=not_missing,
shared=shared,
not_shared=not_shared,
photos=photos,
movies=movies,
uti=uti,
burst=burst,
not_burst=not_burst,
live=live,
not_live=not_live,
cloudasset=cloudasset,
not_cloudasset=not_cloudasset,
incloud=incloud,
not_incloud=not_incloud,
from_date=from_date,
to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait,
not_portrait=not_portrait,
screenshot=screenshot,
not_screenshot=not_screenshot,
slow_mo=slow_mo,
not_slow_mo=not_slow_mo,
time_lapse=time_lapse,
not_time_lapse=not_time_lapse,
hdr=hdr,
not_hdr=not_hdr,
selfie=selfie,
not_selfie=not_selfie,
panorama=panorama,
not_panorama=not_panorama,
has_raw=has_raw,
place=place,
no_place=no_place,
location=location,
no_location=no_location,
label=label,
deleted=deleted,
deleted_only=deleted_only,
has_comment=has_comment,
no_comment=no_comment,
has_likes=has_likes,
no_likes=no_likes,
is_reference=is_reference,
in_album=in_album,
not_in_album=not_in_album,
name=name,
min_size=min_size,
max_size=max_size,
query_eval=query_eval,
function=query_function,
regex=regex,
selected=selected,
exif=exif,
duplicate=duplicate,
)
try:
photos = photosdb.query(query_options)
except ValueError as e:
if "Invalid query_eval CRITERIA:" in str(e):
msg = str(e).split(":")[1]
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
)
else:
raise ValueError(e)
# below needed for to make CliRunner work for testing
cli_json = cli_obj.json if cli_obj is not None else None
if add_to_album and photos:
album_query = PhotosAlbum(add_to_album, verbose=None)
photo_len = len(photos)
photo_word = "photos" if photo_len > 1 else "photo"
click.echo(
f"Adding {photo_len} {photo_word} to album '{album_query.name}'. Note: Photos may prompt you to confirm this action.",
err=True,
)
try:
album_query.add_list(photos)
except Exception as e:
click.secho(
f"Error adding photos to album {add_to_album}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
print_photo_info(photos, cli_json or json_, print_func=click.echo)

339
osxphotos/cli/repl.py Normal file
View File

@@ -0,0 +1,339 @@
"""repl command for osxphotos CLI"""
import dataclasses
import os
import os.path
import pathlib
import sys
import time
from typing import List
import click
import photoscript
from rich import pretty, print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION
from osxphotos.photoinfo import PhotoInfo
from osxphotos.photosdb import PhotosDB
from osxphotos.pyrepl import embed_repl
from osxphotos.queryoptions import QueryOptions
from .common import (
DB_ARGUMENT,
DB_OPTION,
DELETED_OPTIONS,
QUERY_OPTIONS,
get_photos_db,
load_uuid_from_file,
)
class IncompatibleQueryOptions(Exception):
pass
@click.command(name="repl")
@DB_OPTION
@click.pass_obj
@click.pass_context
@click.option(
"--emacs",
required=False,
is_flag=True,
default=False,
help="Launch REPL with Emacs keybindings (default is vi bindings)",
)
@click.option(
"--beta",
is_flag=True,
default=False,
hidden=True,
help="Enable beta options.",
)
@QUERY_OPTIONS
@DELETED_OPTIONS
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
@click.option(
"--not-missing",
is_flag=True,
help="Search for photos present on disk (e.g. not missing).",
)
@click.option(
"--cloudasset",
is_flag=True,
help="Search for photos that are part of an iCloud library",
)
@click.option(
"--not-cloudasset",
is_flag=True,
help="Search for photos that are not part of an iCloud library",
)
@click.option(
"--incloud",
is_flag=True,
help="Search for photos that are in iCloud (have been synched)",
)
@click.option(
"--not-incloud",
is_flag=True,
help="Search for photos that are not in iCloud (have not been synched)",
)
def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
import logging
from objexplore import explore
from photoscript import Album, Photo, PhotosLibrary
from rich import inspect as _inspect
from osxphotos import ExifTool, PhotoInfo, PhotosDB
from osxphotos.albuminfo import AlbumInfo
from osxphotos.momentinfo import MomentInfo
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
from osxphotos.placeinfo import PlaceInfo
from osxphotos.queryoptions import QueryOptions
from osxphotos.scoreinfo import ScoreInfo
from osxphotos.searchinfo import SearchInfo
logger = logging.getLogger()
logger.disabled = True
pretty.install()
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
db = db or get_photos_db()
photosdb = _load_photos_db(db)
# enable beta features if requested
if beta:
photosdb._beta = beta
print("Beta mode enabled")
print("Getting photos")
tic = time.perf_counter()
try:
query_options = _query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions:
click.echo("Incompatible query options", err=True)
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
sys.exit(1)
photos = _query_photos(photosdb, query_options)
all_photos = _get_all_photos(photosdb)
toc = time.perf_counter()
tictoc = toc - tic
# shortcut for helper functions
get_photo = photosdb.get_photo
show = _show_photo
spotlight = _spotlight_photo
get_selected = _get_selected(photosdb)
try:
selected = get_selected()
except Exception:
# get_selected sometimes fails
selected = []
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
print("The following classes have been imported from osxphotos:")
print(
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportOptions, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
)
print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for '{photosdb.library_path}'")
print(
f"- photos: list of PhotoInfo objects for all photos filtered with any query options passed on command line (len={len(photos)})"
)
print(
f"- all_photos: list of PhotoInfo objects for all photos in photosdb, including those in the trash (len={len(all_photos)})"
)
print(
f"- selected: list of PhotoInfo objects for any photos selected in Photos (len={len(selected)})"
)
print(f"\nThe following functions may be helpful:")
print(
f"- get_photo(uuid): return a PhotoInfo object for photo with uuid; e.g. get_photo('B13F4485-94E0-41CD-AF71-913095D62E31')"
)
print(
f"- get_selected(); return list of PhotoInfo objects for photos selected in Photos"
)
print(
f"- show(photo): open a photo object in the default viewer; e.g. show(selected[0])"
)
print(
f"- show(path): open a file at path in the default viewer; e.g. show('/path/to/photo.jpg')"
)
print(f"- spotlight(photo): open a photo and spotlight it in Photos")
# print(
# f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
# )
print(
f"- inspect(object): print information about an object; e.g. inspect(PhotoInfo)"
)
print(
f"- explore(object): interactively explore an object with objexplore; e.g. explore(PhotoInfo)"
)
print(f"- q, quit, quit(), exit, exit(): exit this interactive shell\n")
embed_repl(
globals=globals(),
locals=locals(),
history_filename=str(pathlib.Path.home() / ".osxphotos_repl_history"),
quit_words=["q", "quit", "exit"],
vi_mode=not emacs,
)
def _show_photo(photo: PhotoInfo):
"""open image with default image viewer
Note: This is for debugging only -- it will actually open any filetype which could
be very, very bad.
Args:
photo: PhotoInfo object or a path to a photo on disk
"""
photopath = photo.path if isinstance(photo, osxphotos.PhotoInfo) else photo
if not os.path.isfile(photopath):
return f"'{photopath}' does not appear to be a valid photo path"
os.system(f"open '{photopath}'")
def _load_photos_db(dbpath):
print("Loading database")
tic = time.perf_counter()
photosdb = osxphotos.PhotosDB(dbfile=dbpath, verbose=print)
toc = time.perf_counter()
tictoc = toc - tic
print(f"Done: took {tictoc:0.2f} seconds")
return photosdb
def _get_all_photos(photosdb):
"""get list of all photos in photosdb"""
photos = photosdb.photos(images=True, movies=True)
photos.extend(photosdb.photos(images=True, movies=True, intrash=True))
return photos
def _get_selected(photosdb):
"""get list of PhotoInfo objects for photos selected in Photos"""
def get_selected():
selected = photoscript.PhotosLibrary().selection
if not selected:
return []
return photosdb.photos(uuid=[p.uuid for p in selected])
return get_selected
def _spotlight_photo(photo: PhotoInfo):
photo_ = photoscript.Photo(photo.uuid)
photo_.spotlight()
def _query_options_from_kwargs(**kwargs) -> QueryOptions:
"""Validate query options and create a QueryOptions instance"""
# sanity check input args
nonexclusive = [
"keyword",
"person",
"album",
"folder",
"name",
"uuid",
"uuid_from_file",
"edited",
"external_edit",
"uti",
"has_raw",
"from_date",
"to_date",
"from_time",
"to_time",
"label",
"is_reference",
"query_eval",
"query_function",
"min_size",
"max_size",
"regex",
"selected",
"exif",
"duplicate",
]
exclusive = [
("favorite", "not_favorite"),
("hidden", "not_hidden"),
("missing", "not_missing"),
("only_photos", "only_movies"),
("burst", "not_burst"),
("live", "not_live"),
("cloudasset", "not_cloudasset"),
("incloud", "not_incloud"),
("portrait", "not_portrait"),
("screenshot", "not_screenshot"),
("slow_mo", "not_slow_mo"),
("time_lapse", "not_time_lapse"),
("hdr", "not_hdr"),
("selfie", "not_selfie"),
("panorama", "not_panorama"),
("deleted", "deleted_only"),
("shared", "not_shared"),
("has_comment", "no_comment"),
("has_likes", "no_likes"),
("in_album", "not_in_album"),
("location", "no_location"),
]
# print help if no non-exclusive term or a double exclusive term is given
# TODO: add option to validate requiring at least one query arg
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
[
all([any(kwargs["title"]), kwargs["no_title"]]),
all([any(kwargs["description"]), kwargs["no_description"]]),
all([any(kwargs["place"]), kwargs["no_place"]]),
]
):
raise IncompatibleQueryOptions
# actually have something to query
include_photos = True
include_movies = True # default searches for everything
if kwargs["only_movies"]:
include_photos = False
if kwargs["only_photos"]:
include_movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
uuid = None
if kwargs["uuid_from_file"]:
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
uuid = tuple(uuid_list)
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
query_dict = {field: kwargs.get(field) for field in query_fields}
query_dict["photos"] = include_photos
query_dict["movies"] = include_movies
query_dict["uuid"] = uuid
return QueryOptions(**query_dict)
def _query_photos(photosdb: PhotosDB, query_options: QueryOptions) -> List:
"""Query photos given a QueryOptions instance"""
try:
photos = photosdb.query(query_options)
except ValueError as e:
if "Invalid query_eval CRITERIA:" not in str(e):
raise ValueError(e) from e
msg = str(e).split(":")[1]
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
) from e
return photos

View File

@@ -0,0 +1,68 @@
"""rich Progress bar factory that can return a rich Progress bar or a mock Progress bar"""
import os
from typing import Any, Optional, Union
from rich.console import Console
from rich.progress import GetTimeCallable, Progress, ProgressColumn, TaskID
# set to 1 if running tests
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
class MockProgress:
def __init__(self):
pass
def add_task(
self,
description: str,
start: bool = True,
total: float = 100.0,
completed: int = 0,
visible: bool = True,
**fields: Any,
) -> TaskID:
pass
def advance(self, task_id: TaskID, advance: float = 1) -> None:
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def rich_progress(
*columns: Union[str, ProgressColumn],
console: Optional[Console] = None,
auto_refresh: bool = True,
refresh_per_second: float = 10,
speed_estimate_period: float = 30.0,
transient: bool = False,
redirect_stdout: bool = True,
redirect_stderr: bool = True,
get_time: Optional[GetTimeCallable] = None,
disable: bool = False,
expand: bool = False,
mock: bool = False,
) -> None:
"""Return a rich.progress.Progress object unless mock=True or os.getenv("OSXPHOTOS_IS_TESTING") is set"""
# if OSXPHOTOS_IS_TESTING is set or mock=True, return a MockProgress object
if mock or OSXPHOTOS_IS_TESTING:
return MockProgress()
return Progress(
*columns,
console=console,
auto_refresh=auto_refresh,
refresh_per_second=refresh_per_second,
speed_estimate_period=speed_estimate_period,
transient=transient,
redirect_stdout=redirect_stdout,
redirect_stderr=redirect_stderr,
get_time=get_time,
disable=disable,
expand=expand,
)

158
osxphotos/cli/snap_diff.py Normal file
View File

@@ -0,0 +1,158 @@
"""snap/diff commands for osxphotos CLI"""
import datetime
import os
import pathlib
import shutil
import subprocess
import click
from rich.console import Console
from rich.syntax import Syntax
import osxphotos
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
from .verbose import verbose_print
@click.command(name="snap")
@click.pass_obj
@click.pass_context
@DB_OPTION
def snap(ctx, cli_obj, db):
"""Create snapshot of Photos database to use with diff command
Snapshots only the database files, not the entire library. If OSXPHOTOS_SNAPSHOT
environment variable is defined, will use that as snapshot directory, otherwise
uses '/private/tmp/osxphotos_snapshots'
Works only on Photos library versions since Catalina (10.15) or newer.
"""
db = get_photos_db(db, cli_obj.db)
db_path = pathlib.Path(db)
if db_path.is_file():
# assume it's the sqlite file
db_path = db_path.parent.parent
db_path = db_path / "database"
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
if not os.path.isdir(db_folder):
click.echo(f"Creating snapshot folder: '{db_folder}'")
os.mkdir(db_folder)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
destination_path = pathlib.Path(db_folder) / timestamp
# get all the sqlite files including the write ahead log if any
files = db_path.glob("*.sqlite*")
os.makedirs(destination_path)
fu = osxphotos.fileutil.FileUtil()
count = 0
for file in files:
if file.is_file():
fu.copy(file, destination_path)
count += 1
print(f"Copied {count} files from {db_path} to {destination_path}")
@click.command(name="diff")
@click.pass_obj
@click.pass_context
@DB_OPTION
@click.option(
"--raw-output",
"-r",
is_flag=True,
default=False,
help="Print raw output (don't use syntax highlighting).",
)
@click.option(
"--style",
"-s",
metavar="STYLE",
nargs=1,
default="monokai",
help="Specify style/theme for syntax highlighting. "
"Theme may be any valid pygments style (https://pygments.org/styles/). "
"Default is 'monokai'.",
)
@click.argument("db2", nargs=-1, type=click.Path(exists=True))
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
"""Compare two Photos databases and print out differences
To use the diff command, you'll need to install sqldiff via homebrew:
- Install homebrew (https://brew.sh/) if not already installed
- Install sqldiff: `brew install sqldiff`
When run with no arguments, compares the current Photos library to the
most recent snapshot in the the OSXPHOTOS_SNAPSHOT directory.
If run with the --db option, compares the library specified by --db to the
most recent snapshot in the the OSXPHOTOS_SNAPSHOT directory.
If run with just the DB2 argument, compares the current Photos library to
the database specified by the DB2 argument.
If run with both the --db option and the DB2 argument, compares the
library specified by --db to the database specified by DB2
See also `osxphotos snap`
If the OSXPHOTOS_SNAPSHOT environment variable is not set, will use
'/private/tmp/osxphotos_snapshots'
Works only on Photos library versions since Catalina (10.15) or newer.
"""
verbose_ = verbose_print(verbose, rich=True)
sqldiff = shutil.which("sqldiff")
if not sqldiff:
click.echo(
"sqldiff not found; install via homebrew (https://brew.sh/): `brew install sqldiff`"
)
ctx.exit(2)
verbose_(f"sqldiff found at '{sqldiff}'")
db = get_photos_db(db, cli_obj.db)
db_path = pathlib.Path(db)
if db_path.is_file():
# assume it's the sqlite file
db_path = db_path.parent.parent
db_path = db_path / "database"
db_1 = db_path / "photos.sqlite"
if db2:
db_2 = pathlib.Path(db2[0])
else:
# get most recent snapshot
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
verbose_(f"Using snapshot folder: '{db_folder}'")
folders = sorted([f for f in pathlib.Path(db_folder).glob("*") if f.is_dir()])
folder_2 = folders[-1]
db_2 = folder_2 / "Photos.sqlite"
if not db_1.exists():
print(f"database file {db_1} missing")
if not db_2.exists():
print(f"database file {db_2} missing")
verbose_(f"Comparing databases {db_1} and {db_2}")
diff_proc = subprocess.Popen([sqldiff, db_2, db_1], stdout=subprocess.PIPE)
console = Console()
for line in iter(diff_proc.stdout.readline, b""):
line = line.decode("UTF-8").rstrip()
if raw_output:
print(line)
else:
syntax = Syntax(
line, "sql", theme=style, line_numbers=False, code_width=1000
)
console.print(syntax)

132
osxphotos/cli/theme.py Normal file
View File

@@ -0,0 +1,132 @@
"""theme command for osxphotos for managing color themes"""
import pathlib
import click
from rich.console import Console
from rich_theme_manager import Theme
from .click_rich_echo import rich_click_echo
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
from .help import get_help_msg
@click.command(name="theme")
@click.pass_obj
@click.pass_context
@click.option("--default", is_flag=True, help="Show default theme.")
@click.option("--list", "list_", is_flag=True, help="List all themes.")
@click.option(
"--config",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Print configuration for THEME (or default theme if not specified).",
)
@click.option(
"--preview",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Preview THEME (or default theme if not specified).",
)
@click.option(
"--edit",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Edit THEME (or default theme if not specified).",
)
@click.option(
"--clone",
metavar="THEME NEW_THEME",
nargs=2,
type=str,
help="Clone THEME to NEW_THEME.",
)
@click.option("--delete", metavar="THEME", help="Delete THEME.")
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
"""Manage osxphotos color themes."""
subcommands = [default, list_, config, preview, edit, clone, delete]
subcommand_names = (
"--default, --list, --config, --preview, --edit, --clone, --delete"
)
if not any(subcommands):
click.echo(
f"Must specify exactly one of: {subcommand_names}\n",
err=True,
)
rich_click_echo(get_help_msg(theme), err=True)
return
if sum(bool(cmd) for cmd in subcommands) != 1:
# only a single subcommand may be specified
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
theme_manager = get_theme_manager()
console = Console(theme=get_default_theme())
if default:
default = get_default_theme()
theme_manager.list_themes(theme_names=[default.name])
return
if list_:
theme_manager.list_themes()
return
if config:
if config == "_DEFAULT_":
print(get_default_theme().config)
else:
print(get_theme(config).config)
return
if preview:
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
theme_manager.preview_theme(theme_)
return
if edit:
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
config_file = pathlib.Path(theme_.path)
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
click.edit(filename=str(config_file))
return
if clone:
src_theme = get_theme(clone[0])
dest_path = get_theme_dir() / f"{clone[1]}.theme"
if dest_path.exists():
raise click.ClickException(
f"Theme '{clone[1]}' already exists at {dest_path}"
)
dest_theme = Theme(
name=clone[1],
description=src_theme.description,
inherit=src_theme.inherit,
tags=src_theme.tags,
styles={
style_name: src_theme.styles[style_name]
for style_name in src_theme.style_names
},
)
theme_manager = get_theme_manager()
theme_manager.add(dest_theme)
theme_ = get_theme(dest_theme.name)
console.print(
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
f"at [filepath]{theme_.path}[/]"
)
return
if delete:
theme_ = get_theme(delete)
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
theme_manager.remove(theme_)
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
return

46
osxphotos/cli/tutorial.py Normal file
View File

@@ -0,0 +1,46 @@
"""tutorial command for osxphotos CLI"""
import io
import pathlib
import click
from rich.console import Console
from rich.markdown import Markdown
from .help import strip_html_comments, strip_md_links
@click.command(name="tutorial")
@click.argument(
"WIDTH",
nargs=-1,
type=click.INT,
)
@click.pass_obj
@click.pass_context
def tutorial(ctx, cli_obj, width):
"""Display osxphotos tutorial."""
width = width[0] if width else 100
click.echo_via_pager(tutorial_help(width=width))
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 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

26
osxphotos/cli/uuid.py Normal file
View File

@@ -0,0 +1,26 @@
"""uuid command for osxphotos CLI"""
import click
import photoscript
@click.command(name="uuid")
@click.pass_obj
@click.pass_context
@click.option(
"--filename",
"-f",
required=False,
is_flag=True,
default=False,
help="Include filename of selected photos in output",
)
def uuid(ctx, cli_obj, filename):
"""Print out unique IDs (UUID) of photos selected in Photos
Prints outs UUIDs in form suitable for --uuid-from-file and --skip-uuid-from-file
"""
for photo in photoscript.PhotosLibrary().selection:
if filename:
print(f"# {photo.filename}")
print(photo.uuid)

143
osxphotos/cli/verbose.py Normal file
View File

@@ -0,0 +1,143 @@
"""helper functions for printing verbose output"""
import os
import typing as t
from datetime import datetime
import click
from rich.console import Console
from rich.theme import Theme
from .click_rich_echo import rich_click_echo
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, time_stamp
# set to 1 if running tests
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
# include error/warning emoji's in verbose output
ERROR_EMOJI = True
__all__ = ["get_verbose_console", "verbose_print"]
class _Console:
"""Store console object for verbose output"""
def __init__(self):
self._console: t.Optional[Console] = None
@property
def console(self):
return self._console
@console.setter
def console(self, console: Console):
self._console = console
_console = _Console()
def noop(*args, **kwargs):
"""no-op function"""
pass
def get_verbose_console() -> Console:
"""Get console object
Returns:
Console object
"""
global _console
if _console.console is None:
_console.console = Console(force_terminal=True)
return _console.console
def verbose_print(
verbose: bool = True,
timestamp: bool = False,
rich: bool = False,
highlight: bool = False,
theme: t.Optional[Theme] = None,
**kwargs: t.Any,
) -> t.Callable:
"""Create verbose function to print output
Args:
verbose: if True, returns verbose print function otherwise returns no-op function
timestamp: if True, includes timestamp in verbose output
rich: use rich.print instead of click.echo
highlight: if True, use automatic rich.print highlighting
theme: optional rich.theme.Theme object to use for formatting
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
Returns:
function to print output
"""
if not verbose:
return noop
global _console
_console.console = Console(theme=theme, width=10_000)
# closure to capture timestamp
def verbose_(*args):
"""print output if verbose flag set"""
styled_args = []
timestamp_str = f"{str(datetime.now())} -- " if timestamp else ""
for arg in args:
if type(arg) == str:
arg = timestamp_str + arg
if "error" in arg.lower():
arg = click.style(arg, fg=CLI_COLOR_ERROR)
elif "warning" in arg.lower():
arg = click.style(arg, fg=CLI_COLOR_WARNING)
styled_args.append(arg)
click.echo(*styled_args, **kwargs)
def rich_verbose_(*args):
"""rich.print output if verbose flag set"""
global ERROR_EMOJI
timestamp_str = time_stamp() if timestamp else ""
new_args = []
for arg in args:
if type(arg) == str:
if "error" in arg.lower():
arg = f"[error]{arg}"
if ERROR_EMOJI:
arg = f":cross_mark-emoji: {arg}"
elif "warning" in arg.lower():
arg = f"[warning]{arg}"
if ERROR_EMOJI:
arg = f":warning-emoji: {arg}"
arg = timestamp_str + arg
new_args.append(arg)
_console.console.print(*new_args, highlight=highlight, **kwargs)
def rich_verbose_testing_(*args):
"""print output if verbose flag set using rich.print"""
global ERROR_EMOJI
timestamp_str = time_stamp() if timestamp else ""
new_args = []
for arg in args:
if type(arg) == str:
if "error" in arg.lower():
arg = f"[error]{arg}"
if ERROR_EMOJI:
arg = f":cross_mark-emoji: {arg}"
elif "warning" in arg.lower():
arg = f"[warning]{arg}"
if ERROR_EMOJI:
arg = f":warning-emoji: {arg}"
arg = timestamp_str + arg
new_args.append(arg)
rich_click_echo(*new_args, theme=theme, **kwargs)
if rich and not OSXPHOTOS_IS_TESTING:
return rich_verbose_
elif rich:
return rich_verbose_testing_
else:
return verbose_

View File

@@ -1,9 +1,17 @@
""" ConfigOptions class to load/save config settings for osxphotos CLI """
import bitmath
import toml
__all__ = [
"ConfigOptionsException",
"ConfigOptionsInvalidError",
"ConfigOptionsLoadError",
"ConfigOptions",
]
class ConfigOptionsException(Exception):
""" Invalid combination of options. """
"""Invalid combination of options."""
def __init__(self, message):
self.message = message
@@ -19,10 +27,10 @@ class ConfigOptionsLoadError(ConfigOptionsException):
class ConfigOptions:
""" data class to store and load options for osxphotos commands """
"""data class to store and load options for osxphotos commands"""
def __init__(self, name, attrs, ignore=None):
""" init ConfigOptions class
"""init ConfigOptions class
Args:
name: name for these options, will be used for section heading in TOML file when saving/loading from file
@@ -53,21 +61,21 @@ class ConfigOptions:
raise KeyError(f"Missing argument: {attr}")
def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False):
""" validate combinations of otions
"""validate combinations of otions
Args:
exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive;
ie. either option_1 can be set or option_2 but not both;
inclusive: list of tuples in form [("option_1", "option_2")...] which are inclusive;
exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive;
ie. either option_1 can be set or option_2 but not both;
inclusive: list of tuples in form [("option_1", "option_2")...] which are inclusive;
ie. if either option_1 or option_2 is set, the other must be set
dependent: list of tuples in form [("option_1", ("option_2", "option_3"))...]
dependent: list of tuples in form [("option_1", ("option_2", "option_3"))...]
where if option_1 is set, then at least one of the options in the second tuple must also be set
cli: bool, set to True if called to validate CLI options;
cli: bool, set to True if called to validate CLI options;
will prepend '--' to option names in InvalidOptions.message and change _ to - in option names
Returns:
True if all options valid
Raises:
InvalidOption if any combination of options is invalid
InvalidOption.message will be descriptive message of invalid options
@@ -121,27 +129,21 @@ class ConfigOptions:
return True
def write_to_file(self, filename):
""" Write self to TOML file
"""Write self to TOML file
Args:
filename: full path to TOML file to write; filename will be overwritten if it exists
"""
# todo: add overwrite and option to merge contents already in TOML file (under different [section] with new content)
data = {}
for attr in sorted(self._attrs.keys()):
val = getattr(self, attr)
if val in [False, ()]:
val = None
else:
val = list(val) if type(val) == tuple else val
data[attr] = val
with open(filename, "w") as fd:
toml.dump({self._name: data}, fd)
toml.dump(self._get_toml_dict(), fd)
def write_to_str(self) -> str:
"""Write self to TOML str"""
return toml.dumps(self._get_toml_dict())
def load_from_file(self, filename, override=False):
""" Load options from a TOML file.
"""Load options from a TOML file.
Args:
filename: full path to TOML file
@@ -171,3 +173,21 @@ class ConfigOptions:
def asdict(self):
return {attr: getattr(self, attr) for attr in sorted(self._attrs.keys())}
def _get_toml_dict(self):
"""Return dict for writing to TOML file"""
data = {}
for attr in sorted(self._attrs.keys()):
val = getattr(self, attr)
if isinstance(val, bitmath.Bitmath):
val = int(val.to_Byte())
if val in [False, ()]:
val = None
else:
val = list(val) if type(val) == tuple else val
data[attr] = val
return {self._name: data}

View File

@@ -0,0 +1,60 @@
"""Error logger/crash reporter decorator"""
import datetime
import functools
import platform
import sys
import traceback
from rich import print
from ._version import __version__
# store data to print out in crash log, set by set_crash_data
CRASH_DATA = {}
def set_crash_data(key_, data):
"""Set data to be printed in crash log"""
CRASH_DATA[key_] = data
def crash_reporter(filename, message, title, postamble, *extra_args):
"""Create a crash dump file on error named filename
On error, create a crash dump file named filename with exception and stack trace.
message is printed to stderr
title is printed at beginning of crash dump file
postamble is printed to stderr after crash dump file is created
If extra_args is not None, any additional arguments to the function will be printed to the file.
"""
def decorated(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(message, file=sys.stderr)
print(f"[red]{e}[/red]", file=sys.stderr)
with open(filename, "w") as f:
f.write(f"{title}\n")
f.write(f"Created: {datetime.datetime.now()}\n")
f.write(f"osxphotos version: {__version__}\n")
f.write(f"Platform: {platform.platform()}\n")
f.write(f"Python version: {sys.version}\n")
f.write(f"sys.argv: {sys.argv}\n")
f.write("CRASH_DATA: \\n")
for k, v in CRASH_DATA.items():
f.write(f"{k}: {v}\n")
for arg in extra_args:
f.write(f"{arg}\n")
f.write(f"Error: {e}\n")
traceback.print_exc(file=f)
print(f"Crash log written to '{filename}'", file=sys.stderr)
print(f"{postamble}", file=sys.stderr)
sys.exit(1)
return wrapped
return decorated

View File

@@ -2,69 +2,71 @@
import datetime
__all__ = ["DateTimeFormatter"]
class DateTimeFormatter:
""" provides property access to formatted datetime.datetime strftime values """
"""provides property access to formatted datetime.datetime strftime values"""
def __init__(self, dt: datetime.datetime):
self.dt = dt
@property
def date(self):
""" ISO date in form 2020-03-22 """
"""ISO date in form 2020-03-22"""
return self.dt.date().isoformat()
@property
def year(self):
""" 4 digit year """
"""4 digit year"""
return f"{self.dt.year}"
@property
def yy(self):
""" 2 digit year """
"""2 digit year"""
return f"{self.dt.strftime('%y')}"
@property
def mm(self):
""" 2 digit month """
"""2 digit month"""
return f"{self.dt.strftime('%m')}"
@property
def month(self):
""" Month as locale's full name """
"""Month as locale's full name"""
return f"{self.dt.strftime('%B')}"
@property
def mon(self):
""" Month as locale's abbreviated name """
"""Month as locale's abbreviated name"""
return f"{self.dt.strftime('%b')}"
@property
def dd(self):
""" 2-digit day of the month """
"""2-digit day of the month"""
return f"{self.dt.strftime('%d')}"
@property
def dow(self):
""" Day of week as locale's name """
"""Day of week as locale's name"""
return f"{self.dt.strftime('%A')}"
@property
def doy(self):
""" Julian day of year starting from 001 """
"""Julian day of year starting from 001"""
return f"{self.dt.strftime('%j')}"
@property
def hour(self):
""" 2-digit hour """
"""2-digit hour"""
return f"{self.dt.strftime('%H')}"
@property
def min(self):
""" 2-digit minute """
"""2-digit minute"""
return f"{self.dt.strftime('%M')}"
@property
def sec(self):
""" 2-digit second """
"""2-digit second"""
return f"{self.dt.strftime('%S')}"

View File

@@ -2,13 +2,23 @@
import datetime
__all__ = [
"get_local_tz",
"datetime_has_tz",
"datetime_tz_to_utc",
"datetime_remove_tz",
"datetime_naive_to_utc",
"datetime_naive_to_local",
"datetime_utc_to_local",
]
def get_local_tz(dt):
""" Return local timezone as datetime.timezone tzinfo for dt
"""Return local timezone as datetime.timezone tzinfo for dt
Args:
dt: datetime.datetime
Returns:
local timezone for dt as datetime.timezone
@@ -22,14 +32,14 @@ def get_local_tz(dt):
def datetime_has_tz(dt):
""" Return True if datetime dt has tzinfo else False
"""Return True if datetime dt has tzinfo else False
Args:
dt: datetime.datetime
Returns:
True if dt is timezone aware, else False
Raises:
TypeError if dt is not a datetime.datetime object
"""
@@ -41,15 +51,15 @@ def datetime_has_tz(dt):
def datetime_tz_to_utc(dt):
""" Convert datetime.datetime object with timezone to UTC timezone
"""Convert datetime.datetime object with timezone to UTC timezone
Args:
dt: datetime.datetime object
Returns:
datetime.datetime in UTC timezone
Raises:
Raises:
TypeError if dt is not datetime.datetime object
ValueError if dt does not have timeone information
"""
@@ -64,14 +74,14 @@ def datetime_tz_to_utc(dt):
def datetime_remove_tz(dt):
""" Remove timezone from a datetime.datetime object
"""Remove timezone from a datetime.datetime object
Args:
dt: datetime.datetime object with tzinfo
Returns:
dt without any timezone info (naive datetime object)
dt without any timezone info (naive datetime object)
Raises:
TypeError if dt is not a datetime.datetime object
"""
@@ -83,15 +93,15 @@ def datetime_remove_tz(dt):
def datetime_naive_to_utc(dt):
""" Convert naive (timezone unaware) datetime.datetime
"""Convert naive (timezone unaware) datetime.datetime
to aware timezone in UTC timezone
Args:
dt: datetime.datetime without timezone
Returns:
datetime.datetime with UTC timezone
Raises:
TypeError if dt is not a datetime.datetime object
ValueError if dt is not a naive/timezone unaware object
@@ -111,15 +121,15 @@ def datetime_naive_to_utc(dt):
def datetime_naive_to_local(dt):
""" Convert naive (timezone unaware) datetime.datetime
"""Convert naive (timezone unaware) datetime.datetime
to aware timezone in local timezone
Args:
dt: datetime.datetime without timezone
Returns:
datetime.datetime with local timezone
Raises:
TypeError if dt is not a datetime.datetime object
ValueError if dt is not a naive/timezone unaware object
@@ -139,7 +149,7 @@ def datetime_naive_to_local(dt):
def datetime_utc_to_local(dt):
""" Convert datetime.datetime object in UTC timezone to local timezone
"""Convert datetime.datetime object in UTC timezone to local timezone
Args:
dt: datetime.datetime object

103
osxphotos/debug.py Normal file
View File

@@ -0,0 +1,103 @@
"""Utilities for debugging"""
import logging
import sys
import time
from datetime import datetime
from typing import Dict, List
import wrapt
from rich import print
# global variable to control debug output
# set via --debug
DEBUG = False
def set_debug(debug: bool):
"""set debug flag"""
global DEBUG
DEBUG = debug
logging.disable(logging.NOTSET if debug else logging.DEBUG)
def is_debug():
"""return debug flag"""
return DEBUG
def debug_watch(wrapped, instance, args, kwargs):
"""For use with wrapt.wrap_function_wrapper to watch calls to a function"""
caller = sys._getframe().f_back.f_code.co_name
name = wrapped.__name__
timestamp = datetime.now().isoformat()
print(
f"{timestamp} {name} called from {caller} with args: {args} and kwargs: {kwargs}"
)
start_t = time.perf_counter()
rv = wrapped(*args, **kwargs)
stop_t = time.perf_counter()
print(f"{timestamp} {name} returned: {rv}, elapsed time: {stop_t - start_t} sec")
return rv
def debug_breakpoint(wrapped, instance, args, kwargs):
"""For use with wrapt.wrap_function_wrapper to set breakpoint on a function"""
breakpoint()
return wrapped(*args, **kwargs)
def wrap_function(function_path, wrapper):
"""Wrap a function with wrapper function"""
module, name = function_path.split(".", 1)
try:
return wrapt.wrap_function_wrapper(module, name, wrapper)
except AttributeError as e:
raise AttributeError(f"{module}.{name} does not exist") from e
def get_debug_options(arg_names: List, argv: List) -> Dict:
"""Get the options for the debug options;
Some of the debug options like --watch and --breakpoint need to be processed before any other packages are loaded
so they can't be handled in the normal click argument processing, thus this function is called
from osxphotos/cli/__init__.py
Assumes multi-valued options are OK and that all options take form of --option VALUE or --option=VALUE
"""
# argv[0] is the program name
# argv[1] is the command
# argv[2:] are the arguments
args = {}
for arg_name in arg_names:
for idx, arg in enumerate(argv[1:]):
if arg.startswith(f"{arg_name}="):
arg_value = arg.split("=")[1]
try:
args[arg].append(arg_value)
except KeyError:
args[arg] = [arg_value]
elif arg == arg_name:
try:
args[arg].append(argv[idx + 2])
except KeyError:
try:
args[arg] = [argv[idx + 2]]
except IndexError as e:
raise ValueError(f"Missing value for {arg}") from e
except IndexError as e:
raise ValueError(f"Missing value for {arg}") from e
return args
def get_debug_flags(arg_names: List, argv: List) -> Dict:
"""Get the flags for the debug options;
Processes flags like --debug that resolve to True or False
"""
# argv[0] is the program name
# argv[1] is the command
# argv[2:] are the arguments
args = {arg_name: False for arg_name in arg_names}
for arg_name in arg_names:
if arg_name in argv[1:]:
args[arg_name] = True
return args

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: d1a90e805d591609a2d39f1deed3a0fe
tags: 645f666f9bcd5a90fca523b33c5a78b7

3
osxphotos/docs/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Docs for osxphotos
These files are used by `osxphotos docs` command. They are generated by sphinx using `make docs`. Do not directly edit or modify these files.

View File

@@ -0,0 +1,101 @@
<!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.47.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">
<h1>All modules for which code is available</h1>
<ul><li><a href="osxphotos/photoinfo.html">osxphotos.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.4.0</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,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,8 @@
osxphotos command line interface (CLI)
======================================
.. click:: osxphotos.cli:cli_main
:prog: osxphotos
:nested: full
.. program-output:: python3 -m osxphotos --help

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
osxphotos/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;
}
}

906
osxphotos/docs/_static/basic.css vendored Normal file
View File

@@ -0,0 +1,906 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2022 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;
white-space: nowrap;
}
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
osxphotos/docs/_static/custom.css vendored Normal file
View File

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

326
osxphotos/docs/_static/doctools.js vendored Normal file
View File

@@ -0,0 +1,326 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Sphinx JavaScript utilities for all documentation.
*
* :copyright: Copyright 2007-2022 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');
var url = new URL(window.location);
url.searchParams.delete('highlight');
window.history.replaceState({}, '', url);
},
/**
* 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();
});

View File

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

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