Compare commits

...

474 Commits

Author SHA1 Message Date
Rhet Turnbull
515df0a5dc Template refactor (#385)
* Initial implementation of new textx parser for template

* Implemented parser as singleton

* Moved grammar to .tx file

* Added filter templates

* Added filter templates

* Added tests for nested templates

* Added tests for filter+path_sep

* Added tests for filter+path_sep

* Added punctuation templates

* Added hook for --replace-keywords

* Updated docs for phototemplate

* Updated docs for phototemplate

* Updated tests data

* Updated tests data

* Updated docs for phototemplate

* Version bump

* Updated CLI help

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-02 22:57:26 +00:00
Rhet Turnbull
48d2223edd Updated tests for ExportDB, #358 2021-02-02 06:53:32 -08:00
Rhet Turnbull
591f9bcc62 Updated sidecar test data 2021-02-02 06:39:34 -08:00
Rhet Turnbull
2284598a24 Added 11.2 to tested versions, #360 2021-02-02 06:33:50 -08:00
Rhet Turnbull
77371b6e5d Fixed documentation, #359 2021-02-02 06:26:15 -08:00
Rhet Turnbull
8dbedef187 Add @davidjroos as a contributor 2021-02-02 06:23:19 -08:00
Rhet Turnbull
5c093c4352 Implemented --only-new, #358 2021-02-01 07:31:36 -08:00
Rhet Turnbull
e4fc3896f9 Fixed docs [skip ci] 2021-01-24 17:35:49 -08:00
Rhet Turnbull
2fb3fa3058 Fixed example for CLI restructure 2021-01-24 15:48:18 -08:00
Rhet Turnbull
3a4a8bdb0b Restructured docs 2021-01-24 08:27:23 -08:00
Rhet Turnbull
2fed1ebe5e Updated sphinx config 2021-01-23 22:36:35 -08:00
Rhet Turnbull
51f69585be Refactored __main__, added sphinx docs 2021-01-23 22:25:49 -08:00
Rhet Turnbull
8491830ee5 Updated CHANGELOG.md, [skip ci] 2021-01-23 09:24:48 -08:00
Rhet Turnbull
ebe2fc544d Fixed sidecar test data 2021-01-23 09:19:07 -08:00
Rhet Turnbull
4e47de7589 Version bump 2021-01-23 09:17:54 -08:00
Rhet Turnbull
5a696366fa Fix for issue #348 2021-01-23 09:17:13 -08:00
Rhet Turnbull
44153f0251 Updated CHANGELOG.md, [skip ci] 2021-01-22 21:03:30 -08:00
Rhet Turnbull
a287bfb41f Fix for issue #353, #354 2021-01-22 20:43:41 -08:00
Rhet Turnbull
6d55851f75 Updated test data 2021-01-21 06:30:50 -08:00
Rhet Turnbull
7bdbf9da51 Added python version badge [skip ci] 2021-01-21 06:23:15 -08:00
Rhet Turnbull
75516d91e9 Added python versions to setup.py 2021-01-21 06:21:11 -08:00
Rhet Turnbull
8651fbe2b7 Updated CHANGELOG.md, [skip ci] 2021-01-20 16:02:11 -08:00
Rhet Turnbull
c2bf03a811 Added DESCRIPTION.txt [skip ci] 2021-01-20 15:58:32 -08:00
Rhet Turnbull
0904a6e44d Updated docs 2021-01-20 07:08:08 -08:00
Rhet Turnbull
8a935ffbc0 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-01-20 07:03:39 -08:00
Rhet Turnbull
80f382906f Added face regions to XMP sidecars 2021-01-20 07:03:26 -08:00
Rhet Turnbull
66a4a285f3 Added pycon code formatting 2021-01-19 12:23:27 -08:00
Rhet Turnbull
bdc4b23f42 face region fixes for mirrored images 2021-01-19 06:57:47 -08:00
Rhet Turnbull
3cdfc8700d Updated CHANGELOG.md, [skip ci] 2021-01-18 23:39:30 -08:00
Rhet Turnbull
2f866256ad version bump 2021-01-18 23:26:08 -08:00
Rhet Turnbull
86018d5cc0 Fixed face regions for exif orientation 6, 8 2021-01-18 23:25:32 -08:00
Rhet Turnbull
d657fc6ccd Updated CHANGELOG.md, [skip ci] 2021-01-18 12:09:57 -08:00
Rhet Turnbull
875f79b92d Fixed face region orientation 2021-01-18 12:02:33 -08:00
Rhet Turnbull
3a110bb6d3 Updated documentation for new face region properties 2021-01-18 09:14:29 -08:00
Rhet Turnbull
e73327c164 Updated CHANGELOG.md, [skip ci] 2021-01-18 09:00:58 -08:00
Rhet Turnbull
3799594473 Beta fix for Digikam reading XMP 2021-01-18 08:55:44 -08:00
Rhet Turnbull
db430173b5 Add @martinhrpi as a contributor 2021-01-18 07:32:46 -08:00
Rhet Turnbull
defe5cb61a Updated CHANGELOG.md, [skip ci] 2021-01-17 19:42:23 -08:00
Rhet Turnbull
f58f8dd804 Fixed osxphotos.spec datas 2021-01-17 19:32:23 -08:00
Rhet Turnbull
2773ff7381 Added beta support for face regions in xmp 2021-01-17 19:14:21 -08:00
Rhet Turnbull
348ef54b30 Updated CHANGELOG.md, [skip ci] 2021-01-15 21:35:59 -08:00
Rhet Turnbull
9c18cee37e version bump 2021-01-15 21:21:21 -08:00
Rhet Turnbull
651ed50a07 Added isreference property and --is-reference, #321 2021-01-15 21:20:08 -08:00
Rhet Turnbull
248c95237c Updated CHANGELOG.md, [skip ci] 2021-01-15 14:34:17 -08:00
Rhet Turnbull
ddce731a5d Added retry to use_photos_export, issue #351 2021-01-15 14:13:00 -08:00
Rhet Turnbull
f662495c46 Cleaned up utils 2021-01-15 13:52:35 -08:00
Rhet Turnbull
08bc8a9723 Version bump 2021-01-15 13:50:09 -08:00
Rhet Turnbull
1fd0fe5ea4 Fixed XMP sidecars to conform with exiftool format, #349, #350 2021-01-15 13:49:26 -08:00
Rhet Turnbull
fd5976b75c Added update_readme.py to auto-build README 2021-01-15 07:26:58 -08:00
Rhet Turnbull
088476c591 Added modified.strftime template, refactored test_template.py 2021-01-13 06:19:41 -08:00
Rhet Turnbull
250697c4a2 Updated CHANGELOG.md, [skip ci] 2021-01-12 07:23:08 -08:00
Rhet Turnbull
eba796b684 Updated README.md for BigSur support 2021-01-12 07:22:14 -08:00
Rhet Turnbull
965e10e20f Fixed test for M1, added about command, closes #315 2021-01-12 07:03:07 -08:00
Rhet Turnbull
61f649e59d Update @narensankar0529 as a contributor 2021-01-12 06:19:40 -08:00
Rhet Turnbull
165f9b08f5 Fixed time zone for tests 2021-01-11 21:42:03 -08:00
Rhet Turnbull
039118c1aa Add @narensankar0529 as a contributor 2021-01-11 21:33:06 -08:00
Rhet Turnbull
27f779b16c Added version check for M1 macs 2021-01-11 20:27:51 -08:00
Rhet Turnbull
eec960861e Updated CHANGELOG.md, [skip ci] 2021-01-11 06:49:26 -08:00
Rhet Turnbull
4d924d0826 Completed implementation of --jpeg-ext, fixed --dry-run, closes #330, #346 2021-01-11 06:45:35 -08:00
Rhet Turnbull
55c088eea2 Added --jpeg-ext, implements #330 2021-01-10 09:44:42 -08:00
Rhet Turnbull
ee2750224a Updated CHANGELOG.md, [skip ci] 2021-01-09 18:03:21 -08:00
Rhet Turnbull
db1947dd1e Fixed leaky memory in PhotoKit, issue #276 2021-01-09 17:24:06 -08:00
Rhet Turnbull
248fdbcf02 Updated CHANGELOG.md, [skip ci] 2021-01-09 10:32:32 -08:00
Rhet Turnbull
71cb01572d Add @Rott-Apple as a contributor 2021-01-09 10:28:33 -08:00
Rhet Turnbull
51b1058785 Added PhotoInfo.visible, PhotoInfo.date_trashed, closes #333, #334 2021-01-09 10:20:13 -08:00
Rhet Turnbull
87701822ae Merge pull request #344 from kradalby/write-jpeg-memory-leak
Force cleanup of objects in write_jpeg (fix memory leak)
2021-01-09 08:47:35 -08:00
Kristoffer Dalby
b67f11a3bb Force cleanup of objects with autorelease pool
This commit puts the content of write_jpeg into a autorelease_pool context
provided by PyObjC. This essentially means that the objects should be cleaned up
when the context is exited and prevent them from leaking (memory leak).

https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
2021-01-09 14:44:26 +00:00
Rhet Turnbull
804e13efff Updated README [skip ci] 2021-01-08 10:24:45 -08:00
Rhet Turnbull
504b81b720 Merge pull request #328 from synox/patch-2
doc: Recorded screencast and updated of readme [skip ci]
2021-01-08 07:10:49 -08:00
Rhet Turnbull
538e8b588e Updated CHANGELOG.md, [skip ci] 2021-01-08 07:08:07 -08:00
Rhet Turnbull
c4980fc284 Added README.rst, closes #331 2021-01-08 07:04:07 -08:00
Rhet Turnbull
a7678df397 Updated tests workflow badge link 2021-01-08 06:41:05 -08:00
Rhet Turnbull
e6f45f5949 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-01-08 06:36:07 -08:00
Rhet Turnbull
f8468c63fd Renamed workflow to tests 2021-01-08 06:35:56 -08:00
Rhet Turnbull
0545d5e321 Merge pull request #343 from RhetTbull/all-contributors/add-kradalby
All contributors/add kradalby
2021-01-08 06:26:51 -08:00
Rhet Turnbull
5de9d4f90c Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-01-08 06:23:25 -08:00
Rhet Turnbull
123ebb2cb7 Ensure merge_exif_keywords are str not int 2021-01-08 06:23:17 -08:00
Rhet Turnbull
2d09d382e9 skip action for all-contributors bot 2021-01-08 06:22:01 -08:00
allcontributors[bot]
5e676d3507 docs: update .all-contributorsrc [skip ci] 2021-01-08 14:16:30 +00:00
allcontributors[bot]
935865dc65 docs: update README.md [skip ci] 2021-01-08 14:16:25 +00:00
Rhet Turnbull
193f26bec8 Merge pull request #342 from kradalby/master
Ensure keyword list only contains strings, @all-contributors please add @kradalby for code
2021-01-08 06:06:17 -08:00
Kristoffer Dalby
7b6a0af314 Ensure keyword list only contains string
This commit ensures that every keyword in the keyword list that is to be written
to a sidecar or exif only contains strings. The current implementation fails
with an exception as the "sorted" functions will fail if the list contains a mix
of strings and integers.
2021-01-08 11:33:50 +00:00
Rhet Turnbull
549a9b3572 Updated CHANGELOG.md 2021-01-06 08:33:40 -08:00
Rhet Turnbull
792247b51c Improved handling of deleted photos, #332 2021-01-06 08:24:58 -08:00
Rhet Turnbull
568d1b36a6 Refactored ExportResults 2021-01-06 06:56:26 -08:00
Rhet Turnbull
d78097ccc0 Added error_str to ExportResults 2021-01-05 21:34:46 -08:00
Rhet Turnbull
ad9dcd9ed7 Updated help text for --export-as-hardlink, #287 2021-01-05 21:10:37 -08:00
Rhet Turnbull
fb5fb8ebc7 Added additional warning to _photoinfo_export 2021-01-04 12:37:12 -08:00
Rhet Turnbull
7deac581b1 Added test for Big Sur 16.0.1 database changes 2021-01-03 19:40:34 -08:00
Rhet Turnbull
1173c00ce7 Updated all-contributors 2021-01-03 15:24:40 -08:00
Rhet Turnbull
2bf83e4b1f Updated all-contributors 2021-01-03 15:23:21 -08:00
Aravindo Wingeier
aba50c5c73 doc: fixed toc in readme 2021-01-03 22:40:49 +01:00
Aravindo Wingeier
8ca7719641 added screencast files 2021-01-03 22:38:38 +01:00
Aravindo Wingeier
5dc2eeaf9a Create terminalizer-demo.yml 2021-01-03 22:38:08 +01:00
Aravindo Wingeier
658e8ac096 doc: Recorded screencast and updated of readme
... to be more attractive. Inspired by https://github.com/faressoft/terminalizer/blob/master/README.md
2021-01-03 22:36:11 +01:00
Rhet Turnbull
b93d6822ac Updated README, version 2021-01-03 10:41:47 -08:00
Rhet Turnbull
90b493b7a4 Merge pull request #327 from synox/patch-1
doc: start with examples before the export reference, thanks to @synox
2021-01-03 10:30:28 -08:00
Rhet Turnbull
2480f2a325 Added tag_groups arg to ExifTool.asdict(), issue #324 2021-01-03 10:28:44 -08:00
Aravindo Wingeier
a59bb5b02f remove extra spaces 2021-01-03 19:09:29 +01:00
Aravindo Wingeier
7c8bfc811a Adding back dependency https://github.com/RhetTbull/PhotoScript) 2021-01-03 19:08:32 +01:00
Aravindo Wingeier
7c7bf1be6b doc: start with examples before the export reference
Because the very long export reference might makes it hard to spot the examples.
2021-01-03 19:04:49 +01:00
Rhet Turnbull
b1cab32ff4 Updated dependencies in README.md 2021-01-03 09:48:01 -08:00
Rhet Turnbull
05f111a287 Added exception handling/capture for convert-to-jpeg, issue #322 2021-01-03 09:36:26 -08:00
Rhet Turnbull
83915c65ab Add @synox as a contributor 2021-01-03 09:24:45 -08:00
Rhet Turnbull
22f44f7f40 Merge pull request #326 from synox/master
Make readme easier for beginners, thanks to @synox
2021-01-03 09:21:20 -08:00
Aravindo Wingeier
02ef0f9a25 doc simplify readme 2021-01-03 18:04:51 +01:00
Rhet Turnbull
6347d94dfb Updated CHANGELOG.md 2021-01-03 08:47:16 -08:00
Rhet Turnbull
a32c102d62 Updated CHANGELOG.md 2021-01-03 08:46:36 -08:00
Aravindo Wingeier
38842ff924 Cleanup up the readme
simplify as much as possible
2021-01-03 17:31:42 +01:00
Rhet Turnbull
478715a363 Implemented text replacement for templates, issue #316 2021-01-03 07:38:07 -08:00
Rhet Turnbull
74f1002b9a Updated CHANGELOG.md 2020-12-31 13:11:21 -08:00
Rhet Turnbull
2f57abd23c Fixed modified template to use creation time if no modificationd date, issue #312 2020-12-31 13:07:00 -08:00
Rhet Turnbull
f9a43b92c1 Updated CHANGELOG.md 2020-12-31 12:36:16 -08:00
Rhet Turnbull
bf2a55d7f6 Added --xattr-template, closes #242 2020-12-31 12:33:15 -08:00
Rhet Turnbull
34bb7f2cdc Updated CHANGELOG.md 2020-12-30 20:48:38 -08:00
Rhet Turnbull
3394c52768 Fixed --exiftool-path bug, issue #311, #313 2020-12-30 20:21:05 -08:00
Rhet Turnbull
27282af3b9 Updated CHANGELOG.md 2020-12-30 14:00:31 -08:00
Rhet Turnbull
b7b06b9fdb Merge pull request #310 from RhetTbull/finder_tags
Added Finder tags, partial implementation for issue #242
2020-12-30 13:52:37 -08:00
Rhet Turnbull
29e424575a Added tests for Finder tags 2020-12-30 13:37:15 -08:00
Rhet Turnbull
ea373c4197 Updated requirements.txt 2020-12-30 08:52:58 -08:00
Rhet Turnbull
f25a299309 Updated README for finder tags 2020-12-30 08:51:01 -08:00
Rhet Turnbull
5885b23d32 Initial implementation for Finder tags 2020-12-30 08:32:42 -08:00
Rhet Turnbull
5dccdf7750 Fixed --exiftool-path bug, issue #308 2020-12-30 07:31:07 -08:00
Rhet Turnbull
e9134f84df Updated CHANGELOG.md 2020-12-29 09:51:18 -08:00
Rhet Turnbull
3872e7ae64 Fixed --exiftool-path to work with --exiftool-merge-keywords/persons 2020-12-29 09:47:03 -08:00
Rhet Turnbull
b3e86dffc8 Updated CHANGELOG.md 2020-12-29 09:46:25 -08:00
Rhet Turnbull
4897fc4b05 Added --exiftool-path to CLI 2020-12-29 09:23:51 -08:00
Rhet Turnbull
1dbf22fdc9 Updated CHANGELOG.md 2020-12-29 08:03:21 -08:00
Rhet Turnbull
fa58af8b88 Added exiftool signature to JSON output, issue #303 2020-12-29 07:51:34 -08:00
Rhet Turnbull
9c9bcb08b3 Updated CHANGELOG.md 2020-12-28 15:42:09 -08:00
Rhet Turnbull
b1cb99f83f Added --exiftool-merge-keywords/persons, issue #299, #292 2020-12-28 15:31:31 -08:00
Rhet Turnbull
d3605f6303 Updated CHANGELOG.md 2020-12-28 11:58:00 -08:00
Rhet Turnbull
dce002cdfe Added --sidecar-drop-ext, issue #291 2020-12-28 11:54:02 -08:00
Rhet Turnbull
7bd189e9b2 Updated Template Substitution table 2020-12-28 09:26:38 -08:00
Rhet Turnbull
baa86c77f6 Updated CHANGELOG.md 2020-12-28 09:17:06 -08:00
Rhet Turnbull
0d086bf851 Added searchinfo templates, issue #302 2020-12-28 09:14:08 -08:00
Rhet Turnbull
ade98fc150 Refactored sidecar code 2020-12-28 08:23:23 -08:00
Rhet Turnbull
0d66759b1c Refactored export2 to use sidecar bit field 2020-12-27 22:45:47 -08:00
Rhet Turnbull
d833c14ef4 Added --sidecar exiftool, issue #303 2020-12-27 22:17:56 -08:00
Rhet Turnbull
34841f86c0 Updated CHANGELOG.md 2020-12-27 09:29:32 -08:00
Rhet Turnbull
4cc40d24cf Bug fix for --description-template, issue #304 2020-12-27 09:26:54 -08:00
Rhet Turnbull
1ccf03e158 Updated CHANGELOG.md 2020-12-27 08:45:49 -08:00
Rhet Turnbull
75888cd663 Set XMP:Subject to match Keywords, issue #302 2020-12-27 08:35:30 -08:00
Rhet Turnbull
a08d0725b9 Updated CHANGELOG.md 2020-12-26 08:36:17 -08:00
Rhet Turnbull
f9f699ba35 Fixed city/sub-locality for SearchInfo 2020-12-26 08:31:14 -08:00
Rhet Turnbull
f469cccc4b Updated README.md 2020-12-26 08:12:43 -08:00
Rhet Turnbull
4ece5c0d1c Exposed SearchInfo, closes #121 2020-12-26 08:08:18 -08:00
Rhet Turnbull
9ca5d8f0fd Added version to --verbose, closes #297 2020-12-22 21:05:40 -08:00
Rhet Turnbull
2a49255277 Added --exportdb 2020-12-22 20:42:48 -08:00
Rhet Turnbull
f3b7134af1 Fixed help text 2020-12-21 07:40:42 -08:00
Rhet Turnbull
73716f12cd Updated CHANGELOG.md 2020-12-21 07:35:21 -08:00
Rhet Turnbull
a4bbb6492d Added --exiftool-option to CLI, closes #298 2020-12-21 07:32:38 -08:00
Rhet Turnbull
aca19f4063 Updated CHANGELOG.md 2020-12-20 22:16:06 -08:00
Rhet Turnbull
2ebd4c33ff remove duplicate keywords with --exiftool and --sidecar, closes #294 2020-12-20 22:11:50 -08:00
Rhet Turnbull
da2f91ffc7 Updated CHANGELOG.md 2020-12-20 20:44:38 -08:00
Rhet Turnbull
ef94933dd8 version bump 2020-12-20 20:40:09 -08:00
Rhet Turnbull
e0e8850e56 Added better exiftool error handling, closes #300 2020-12-20 20:37:23 -08:00
Rhet Turnbull
8d1ccda0c8 README.md updates for tested versions 2020-12-17 22:28:17 -08:00
Rhet Turnbull
6171c4d665 Updated CHANGELOG.md 2020-12-17 20:00:34 -08:00
Rhet Turnbull
4678f15bc8 Version bump 2020-12-17 19:48:23 -08:00
Rhet Turnbull
a7c688cfc2 Fixed issue #296 2020-12-17 19:47:22 -08:00
Rhet Turnbull
880a9b67a1 Added additional test cases for #286, --ignore-signature 2020-12-17 15:21:34 -08:00
Rhet Turnbull
d40b16a456 Updated README.md 2020-12-16 21:56:19 -08:00
Rhet Turnbull
dcd2fde6d0 Updated CHANGELOG.md 2020-12-16 21:52:57 -08:00
Rhet Turnbull
ad860b1500 Add @finestream as a contributor 2020-12-16 21:50:47 -08:00
Rhet Turnbull
7ad4db6c15 Help text update 2020-12-16 21:48:47 -08:00
Rhet Turnbull
0f1cc7cc71 Merge pull request #295 from finestream/master
Documentation fix for #293. Thanks to @finestream
2020-12-16 21:42:06 -08:00
Rhet Turnbull
5e6a6cd5fb Updated CHANGELOG.md 2020-12-16 20:27:10 -08:00
Rhet Turnbull
e394d8e6be Implemented --ignore-signature, issue #286 2020-12-16 20:11:01 -08:00
finestream
8237bc8267 Merge pull request #1 from finestream/patch-1
Patch 1
2020-12-16 21:48:22 +01:00
finestream
e097f3aad5 Update __main__.py
Possible fix of Issue RhetTbull/osxphotos#293
2020-12-16 21:25:52 +01:00
finestream
3155045ec8 Update README.md
Fixed language
2020-12-16 21:17:08 +01:00
finestream
4f64eeb996 Update README.md
Possible documentation improvement to Issue #293
2020-12-16 20:58:04 +01:00
Rhet Turnbull
3c14ace826 Updated CHANGELOG.md 2020-12-13 22:30:14 -08:00
Rhet Turnbull
d5730dd8ae Fix for issue #263 2020-12-13 22:18:39 -08:00
Rhet Turnbull
5c1c0c5c5a Updated CHANGELOG.md 2020-12-13 22:05:33 -08:00
Rhet Turnbull
d8593a01e2 Fix for QuickTime date/time, issue #282 2020-12-12 22:13:01 -08:00
Rhet Turnbull
1dffe894ff Updated CHANGELOG.md 2020-12-12 08:08:44 -08:00
Rhet Turnbull
29721dd4f0 Merge pull request #290 from RhetTbull/save_config
Added --save-config, --load-config
2020-12-12 07:58:04 -08:00
Rhet Turnbull
6559c4d8f6 removed extended_attributes reference 2020-12-12 07:53:18 -08:00
Rhet Turnbull
baf45ccd2a This is why I never use branches 2020-12-12 07:51:36 -08:00
Rhet Turnbull
aca85ee2aa Version bump 2020-12-12 07:45:24 -08:00
Rhet Turnbull
9584a9ccc5 Merge branch 'master' into save_config 2020-12-12 07:38:35 -08:00
Rhet Turnbull
182b816e34 Updated README.md for --save-config, --load-config 2020-12-12 07:29:16 -08:00
Rhet Turnbull
0262e0d97e Added tests for configoptions.py 2020-12-12 07:25:50 -08:00
Rhet Turnbull
73f936e061 Added link to discussions 2020-12-11 06:19:05 -08:00
Rhet Turnbull
09687cfca4 Initial test for --save-config, --load-config 2020-12-11 06:12:32 -08:00
Rhet Turnbull
e17ee0e388 Updated CHANGELOG.md 2020-12-10 20:48:19 -08:00
Rhet Turnbull
ec4b53ed9d Refactored FileUtil to use copy-on-write no APFS, issue #287 2020-12-10 20:29:47 -08:00
Rhet Turnbull
d7c81adae8 Updated validate code 2020-12-09 20:17:49 -08:00
Rhet Turnbull
37b1e5ca47 Refactoring of save-config/load-config code 2020-12-08 08:32:37 -08:00
Rhet Turnbull
22355fd446 Initial implementation of configoptions for --save-config, --load-config 2020-12-08 07:22:40 -08:00
Rhet Turnbull
d8de86cb6f Updated CHANGELOG.md 2020-12-06 22:10:20 -08:00
Rhet Turnbull
11f563a479 Fix for issue #262 2020-12-06 22:02:47 -08:00
Rhet Turnbull
f75ed17f9c Updated CHANGELOG.md 2020-12-05 21:36:12 -08:00
Rhet Turnbull
e5d6f21d8e Added --cleanup, issue #262 2020-12-05 21:22:49 -08:00
Rhet Turnbull
d371e63022 Updated CHANGELOG.md 2020-12-05 09:03:29 -08:00
Rhet Turnbull
1b6a03a9f8 Fix for issue #257, #275 2020-12-05 08:55:23 -08:00
Rhet Turnbull
0708a42155 Updated CHANGELOG.md 2020-12-05 07:27:40 -08:00
Rhet Turnbull
69cd236712 Merge branch 'master' of github.com:RhetTbull/osxphotos 2020-12-05 07:19:18 -08:00
Rhet Turnbull
4cce9d4939 Implement fix for issue #282, QuickTime metadata 2020-12-05 07:18:49 -08:00
Rhet Turnbull
cfb07cbfaf Implement fix for issue #282, QuickTime metadata 2020-12-05 07:17:26 -08:00
Rhet Turnbull
1eff6bae9e Updated README.md 2020-12-01 21:19:23 -08:00
Rhet Turnbull
435da2a5dd Updated CHANGELOG.md 2020-11-29 18:43:45 -08:00
Rhet Turnbull
ed3a9711dc Removed --use-photokit authorization check, issue 278 2020-11-29 18:26:55 -08:00
Rhet Turnbull
1bc0926948 Updated CHANGELOG.md 2020-11-29 18:26:00 -08:00
Rhet Turnbull
25eacc7cad Added --missing to export, see issue #277 2020-11-29 15:30:45 -08:00
Rhet Turnbull
d9dcf0917a Catch errors in export_photo 2020-11-28 20:00:10 -08:00
Rhet Turnbull
4f36c7c948 Updated CHANGELOG.md 2020-11-28 09:27:12 -08:00
Rhet Turnbull
d22eaf39ed Added --report option to CLI, implements #253 2020-11-28 09:24:16 -08:00
Rhet Turnbull
adf2ba7678 Updated CHANGELOG.md 2020-11-27 17:00:53 -08:00
Rhet Turnbull
af827d7a57 Updated template values 2020-11-27 16:58:11 -08:00
Rhet Turnbull
48acb42631 Added {exiftool} template, implements issue #259 2020-11-27 16:43:48 -08:00
Rhet Turnbull
eba661acf7 Updated CHANGELOG.md 2020-11-26 19:53:35 -08:00
Rhet Turnbull
399d432a66 Added --original-suffix for issue #263 2020-11-26 18:36:17 -08:00
Rhet Turnbull
4cebc57d60 Updated CHANGELOG.md 2020-11-26 15:26:54 -08:00
Rhet Turnbull
489fea56e9 Added tests for issue #265 2020-11-26 13:21:40 -08:00
Rhet Turnbull
0632a97f55 Simplified sidecar table in export_db 2020-11-26 10:42:10 -08:00
Rhet Turnbull
d5a9f76719 More work on issue #265 2020-11-26 10:15:09 -08:00
Rhet Turnbull
382fca3f92 Initial implementation for issue #265 2020-11-26 09:08:26 -08:00
Rhet Turnbull
a807894095 Removed debug code from _photoinfo_export.py 2020-11-25 21:42:27 -08:00
Rhet Turnbull
559350f71d Updated CHANGELOG.md 2020-11-25 20:55:15 -08:00
Rhet Turnbull
b5195f9d2b version bump 2020-11-25 20:32:36 -08:00
Rhet Turnbull
fa332186ab Fix for missing original_filename, issue #267 2020-11-25 20:31:19 -08:00
Rhet Turnbull
aa2ebf55bb Updated test 2020-11-25 19:04:36 -08:00
Rhet Turnbull
d1fbb9fe86 Updated CHANGELOG.md 2020-11-25 18:58:48 -08:00
Rhet Turnbull
116cb662fb Added test for missing original_filename 2020-11-25 18:32:12 -08:00
Rhet Turnbull
db68defc44 version bump 2020-11-25 17:55:07 -08:00
Rhet Turnbull
7460bc88fc Add @jstrine as a contributor 2020-11-25 17:54:53 -08:00
Rhet Turnbull
dbbbbf10a8 Merge pull request #272 from jstrine/fix_xml_escaping
Add XML escaping to XMP sidecar export, thanks to @jstrine for the fix!
2020-11-25 17:52:22 -08:00
Rhet Turnbull
0633814ab2 Merge pull request #270 from jstrine/fix_gps_xmp
Fix EXIF GPS format for XMP sidecar, thanks to @jstrine for the fix!
2020-11-25 17:51:57 -08:00
Rhet Turnbull
df7d45659a Merge pull request #268 from jstrine/fix_path_none
Continue even if the original filename is None, thanks to @jstrine for the fix!
2020-11-25 17:51:38 -08:00
Jonathan Strine
cec266bba4 Fix tests again
Third times the charm to fix a find-replace error this time.
2020-11-25 19:51:09 -05:00
Jonathan Strine
d0d2e80800 Fix tests for apostrophe
Previous commit didn't reflect all locations and had a copy/paste error.
2020-11-25 19:45:21 -05:00
Jonathan Strine
aafdbea564 Fix test to accomodate for escaped apostrophe 2020-11-25 19:36:09 -05:00
Jonathan Strine
c42050a10c Escape characters which cause XML parsing issues 2020-11-25 19:31:51 -05:00
Jonathan Strine
c27cfb1223 Fix test for XMP sidecar with GPS info 2020-11-25 19:24:56 -05:00
Jonathan Strine
ad144da8a0 Use GPSCoordinate format for geolocation 2020-11-25 18:09:38 -05:00
Jonathan Strine
5352aec3b9 Continue even if the original filename is None
Some photos seemed to be missing the original_filename (returning None).
This fix prevents the traceback.
2020-11-25 17:00:22 -05:00
Rhet Turnbull
e951e5361e Exposed --use-photos-export and --use-photokit 2020-11-25 09:15:16 -08:00
Rhet Turnbull
f7bd1376e1 Updated CHANGELOG.md 2020-11-24 06:50:52 -08:00
Rhet Turnbull
26f96d582c Added photokit export as hidden --use-photokit option 2020-11-23 06:23:19 -08:00
Rhet Turnbull
8cb15d1555 Removed debug statement in _photoinfo_export 2020-11-18 22:03:23 -08:00
Rhet Turnbull
2d9429c8ee Fixed missing data file for photoscript 2020-11-14 14:18:47 -08:00
Rhet Turnbull
3b6dd08d2b Version bump, updated requirements 2020-11-14 13:37:46 -08:00
Rhet Turnbull
3c85f26f90 Moved AppleScript to photoscript 2020-11-14 13:34:50 -08:00
Rhet Turnbull
52c054f81f Updated CHANGELOG.md 2020-11-14 09:32:08 -08:00
Rhet Turnbull
8dc59cbc35 Fixed erroneous attempt to export edited with --download-missing 2020-11-12 06:51:36 -08:00
Rhet Turnbull
802e2f069a version bump 2020-11-12 06:18:56 -08:00
Rhet Turnbull
5d4d7d7db7 Fixed path for photos actually missing off disk 2020-11-12 06:18:28 -08:00
Rhet Turnbull
ea9b41bae4 Avoid copying db files if not necessary 2020-11-11 07:03:57 -08:00
Rhet Turnbull
38397b507b Fix for issue #247 2020-11-08 21:17:19 -08:00
Rhet Turnbull
3636fcbc76 Refactored phototemplate.py to add PATH_SEP option 2020-11-08 16:09:51 -08:00
Rhet Turnbull
a6231e29ff More work on phototemplate.py to add inline expansion 2020-11-08 09:10:09 -08:00
Rhet Turnbull
8c36c6712a Updated CHANGELOG.md 2020-11-07 23:14:34 -08:00
Rhet Turnbull
7fa3704840 Implemented boolean type template fields 2020-11-07 23:06:36 -08:00
Rhet Turnbull
e829212987 Bug fix in handling missing edited photos 2020-11-07 22:47:44 -08:00
Rhet Turnbull
df37a017a8 Fixed message in CLI 2020-11-07 21:33:03 -08:00
Rhet Turnbull
101525c95f Updated CHANGELOG.md 2020-11-07 20:40:27 -08:00
Rhet Turnbull
ae2fd2e3db Implemented issue #255 2020-11-07 18:22:17 -08:00
Rhet Turnbull
9588853ea2 Updated CHANGELOG.md 2020-11-07 09:29:31 -08:00
Rhet Turnbull
9d38885416 Fix for exporting slow mo videos, issue #252 2020-11-07 07:58:37 -08:00
Rhet Turnbull
653b7e6600 Refactored regex in phototemplate 2020-11-06 19:55:03 -08:00
Rhet Turnbull
9429ea8ace Updated CHANGELOG.md 2020-11-04 22:02:32 -08:00
Rhet Turnbull
2202f1b1e9 Refactored exiftool.py 2020-11-04 21:37:20 -08:00
Rhet Turnbull
a509ef18d3 README.md update 2020-11-03 21:32:39 -08:00
Rhet Turnbull
0492f94060 Updated CHANGELOG.md 2020-11-03 19:10:34 -08:00
Rhet Turnbull
cf7fab4c7a Implemented context manager for ExifTool, closes #250 2020-11-03 18:51:09 -08:00
Rhet Turnbull
c7c5320587 Fix for issue #39 2020-11-02 05:53:11 -08:00
Rhet Turnbull
cd710771cd Updated CHANGELOG.md 2020-11-01 09:20:21 -08:00
Rhet Turnbull
663e33bc17 Added --ignore-date-modified flag, issue #247 2020-11-01 09:13:45 -08:00
Rhet Turnbull
3660b6360a Updated CHANGELOG.md 2020-10-31 22:19:34 -07:00
Rhet Turnbull
11459d1da4 Updated --exiftool to set dates/times as Photos does, issue #247 2020-10-31 22:11:00 -07:00
Rhet Turnbull
fd14242022 Version bump 2020-10-31 21:05:42 -07:00
Rhet Turnbull
6ac311199e Partial fix for issue #247 on Mojave 2020-10-31 21:04:44 -07:00
Rhet Turnbull
13df6a2395 Add @hhoeck as a contributor 2020-10-31 10:10:21 -07:00
Rhet Turnbull
28dce72a67 Add @agprimatic as a contributor 2020-10-31 10:10:07 -07:00
Rhet Turnbull
e5548ed160 Add @grundsch as a contributor 2020-10-31 10:09:55 -07:00
Rhet Turnbull
5714509765 Add @dethi as a contributor 2020-10-31 10:09:24 -07:00
Rhet Turnbull
46b62af4e2 Add @jystervinou as a contributor 2020-10-31 10:09:06 -07:00
Rhet Turnbull
01ea88fe57 Add @dmd as a contributor 2020-10-31 10:08:43 -07:00
Rhet Turnbull
e6d043ab65 Add @hshore29 as a contributor 2020-10-31 10:08:18 -07:00
Rhet Turnbull
5b1174db5d Add @PabloKohan as a contributor 2020-10-31 10:07:12 -07:00
Rhet Turnbull
9cff8e89c6 Add @mwort as a contributor 2020-10-31 10:03:13 -07:00
Rhet Turnbull
1553563629 Add @britiscurious as a contributor 2020-10-31 10:00:42 -07:00
Rhet Turnbull
db262f58b0 Updated CHANGELOG.md 2020-10-31 09:01:53 -07:00
Rhet Turnbull
0cce234a8c Fixed handling of date_modified for Catalina, issue #247 2020-10-31 08:46:35 -07:00
Rhet Turnbull
c5dba8c89b Added --has-comment/--has-likes to CLI, issue #240 2020-10-29 21:34:15 -07:00
Rhet Turnbull
603dabb8f4 Cleaned up as_dict/asdict, issue #144, #188 2020-10-27 06:54:42 -07:00
Rhet Turnbull
091f1d9bb4 Updated CHANGELOG.md 2020-10-25 22:25:18 -07:00
Rhet Turnbull
d16932d0fd Updated README.md 2020-10-25 22:24:47 -07:00
Rhet Turnbull
23de6b5890 Added comments/likes, implements #214 2020-10-25 22:12:02 -07:00
Rhet Turnbull
4fe58bf2af fixed test 2020-10-25 09:18:20 -07:00
Rhet Turnbull
d87b8f30a4 Added verbose to PhotosDB(), partial fix for #110 2020-10-25 08:16:54 -07:00
Rhet Turnbull
667c89e32c Cleaned up constructor for PhotosDB 2020-10-24 17:20:46 -07:00
Rhet Turnbull
f9cac05f0d Updated CHANGELOG.md 2020-10-24 13:52:27 -07:00
Rhet Turnbull
48f29e138e Fix for issue #238 2020-10-24 13:45:10 -07:00
Rhet Turnbull
7f2701f6ee Updated CHANGELOG.md 2020-10-24 09:17:16 -07:00
Rhet Turnbull
8551981f68 Fixed shared, not_shared in cli 2020-10-24 09:03:34 -07:00
Rhet Turnbull
a416de29e4 Fix for issue #237 2020-10-21 22:29:16 -07:00
Rhet Turnbull
a960468887 Updated related projects 2020-10-20 22:13:10 -07:00
Rhet Turnbull
ea68229dda Added test for issue #235 2020-10-18 21:34:50 -07:00
Rhet Turnbull
a95193aaa4 Updated README.md with better install instructions 2020-10-18 20:35:40 -07:00
Rhet Turnbull
71ef5e5195 Updated get_shared_photo_comments.py 2020-10-18 16:13:43 -07:00
Rhet Turnbull
53b2498e59 Updated get_shared_photo_comments.py 2020-10-18 16:11:45 -07:00
Rhet Turnbull
15e0914af6 Added get_shared_photo_comments.py to examples 2020-10-18 15:52:18 -07:00
Rhet Turnbull
3b3eb1625e Updated README.md 2020-10-18 14:09:40 -07:00
Rhet Turnbull
338b1501d0 Updated CHANGELOG.md 2020-10-17 23:31:47 -07:00
Rhet Turnbull
bda3a029de Updated README.md 2020-10-17 23:31:09 -07:00
Rhet Turnbull
ff0fdffa9b refactored template code to fix #213 2020-10-17 23:21:08 -07:00
Rhet Turnbull
1332e7b45a Updated CHANGELOG.md 2020-10-15 06:44:03 -07:00
Rhet Turnbull
41b23991df Fix for issue #235, #236 2020-10-15 06:31:13 -07:00
Rhet Turnbull
da100f93a9 Fix for issue #234 2020-10-12 05:59:44 -07:00
Rhet Turnbull
d049967c6b Updated CHANGELOG.md 2020-10-11 22:52:03 -07:00
Rhet Turnbull
dcbf8f25f6 Fix for issue #230 2020-10-11 22:40:16 -07:00
Rhet Turnbull
0d6b68d7ba Updated CHANGELOG.md 2020-10-11 22:27:53 -07:00
Rhet Turnbull
07b08433df Updated tests 2020-10-11 22:24:17 -07:00
Rhet Turnbull
b0171ba6f5 Updated tests 2020-10-11 22:20:42 -07:00
Rhet Turnbull
16305cf233 Merge pull request #233 from RhetTbull/convert_to_jpeg
Convert to jpeg
2020-10-11 21:48:51 -07:00
Rhet Turnbull
fe5185be88 Merge branch 'master' into convert_to_jpeg 2020-10-11 21:41:10 -07:00
Rhet Turnbull
58362020cb Updated docs 2020-10-11 21:27:05 -07:00
Rhet Turnbull
464eae2b98 Updated README.md with raw notes 2020-10-11 11:56:31 -07:00
Rhet Turnbull
b5a9794f6b Added israw, tests for Big Sur 2020-10-11 08:59:49 -07:00
Rhet Turnbull
b32f4b8504 Updates to path, path_raw, uti for RAW+JPEG pairs 2020-10-09 22:15:47 -07:00
Rhet Turnbull
0dd05b8cc1 Updated tests, closes #231 2020-10-07 06:14:16 -07:00
Rhet Turnbull
9515736019 version bump 2020-10-06 06:31:10 -07:00
Rhet Turnbull
42a6373f8d Fix for issue #230 2020-10-06 06:22:17 -07:00
Rhet Turnbull
6413342bdb Updated tests for Mojave 2020-10-05 06:16:35 -07:00
Rhet Turnbull
5f14349964 Updated README.md 2020-10-04 22:25:56 -07:00
Rhet Turnbull
b2b39aa607 Updated tests 2020-10-04 22:18:01 -07:00
Rhet Turnbull
0ddd5234b2 Added uti_raw for 10.14, added tests 2020-10-04 21:29:53 -07:00
Rhet Turnbull
ae0166da04 Added test for Big Sur path_edited 2020-10-04 08:25:22 -07:00
Rhet Turnbull
c389207daa Fixed path_edited for Big Sur 2020-10-04 08:18:10 -07:00
Rhet Turnbull
25141e4945 Updated test for Big Sur 2020-10-03 23:02:50 -07:00
Rhet Turnbull
1b181094ed Working on uti and uti_original 2020-10-03 22:04:35 -07:00
Rhet Turnbull
d406d30414 Updated docs for convert-to-jpeg 2020-10-03 14:21:27 -07:00
Rhet Turnbull
9324d8e795 Updated tests 2020-10-03 13:57:46 -07:00
Rhet Turnbull
4099253c8e Updated tests to run in GitHub actions 2020-10-03 13:48:18 -07:00
Rhet Turnbull
2e652b04d0 Updated requirements.txt 2020-10-03 12:09:31 -07:00
Rhet Turnbull
5a13605f85 Added tests, fixed bug in export_db 2020-10-03 11:58:48 -07:00
Rhet Turnbull
15eb940ff0 Added tests for convert_to_jpeg 2020-10-02 22:23:06 -07:00
Rhet Turnbull
22ecf8279a Updated test library for latest Big Sur beta 2020-10-02 06:41:04 -07:00
Rhet Turnbull
38f201d0fb --convert-to-jpeg initial version working 2020-10-02 06:31:20 -07:00
Rhet Turnbull
08725fd27f Updated CHANGELOG.md 2020-09-28 21:38:51 -07:00
Rhet Turnbull
62d54cc0be Version bump for bug fix 2020-09-28 21:35:17 -07:00
Rhet Turnbull
6883fec2b2 Update README.md 2020-09-28 21:33:58 -07:00
Rhet Turnbull
228dfcdc67 Merge pull request #223 from hhoeck/patch-1
Update exiftool.py to preserve file modification time, thanks to @hhoeck
2020-09-28 21:31:30 -07:00
Rhet Turnbull
c939df7171 Fixed bug related to issue #222 2020-09-28 21:23:47 -07:00
Horst Höck
3d21dadf41 Update exiftool.py
Solve "Param --exiftool ruins --touch-file #222"
2020-09-28 21:49:31 +02:00
Rhet Turnbull
ddc1e69b4a Added HEIC test image 2020-09-26 01:44:32 -07:00
Rhet Turnbull
432da7f139 Added tests for 10.15.6 2020-09-24 21:10:32 -07:00
Rhet Turnbull
aa2cf826c7 Updated CHANGELOG.md 2020-09-13 18:24:23 -07:00
Rhet Turnbull
459d91d7b1 Partial fix for issue #213 2020-09-13 18:15:46 -07:00
Rhet Turnbull
eb00ffd737 Fixed exception handling in export 2020-09-13 12:19:21 -07:00
Rhet Turnbull
a1776fa148 Updated README.md 2020-09-07 06:59:49 -07:00
Rhet Turnbull
f1d20103ff Updated CHANGELOG.md 2020-09-07 06:55:05 -07:00
Rhet Turnbull
5f2d401048 Added --skip-original-if-edited for issue #159 2020-09-07 06:33:37 -07:00
Rhet Turnbull
58b3869a7c Still working on issue #208 2020-09-04 12:47:27 -07:00
Rhet Turnbull
c2fecc9d30 Fixed sidecar collisions, closes #210 2020-08-31 06:30:44 -07:00
Rhet Turnbull
1f343c1c11 Updated CHANGELOG.md 2020-08-31 05:43:19 -07:00
Rhet Turnbull
a36eb416b1 Normalize unicode for issue #208 2020-08-31 05:24:54 -07:00
Rhet Turnbull
c9b15186a0 Updated README.md 2020-08-29 22:04:09 -07:00
Rhet Turnbull
315fe6a6a3 Merge pull request #212 from dmd/patch-1
typo fix - thanks to @dmd
2020-08-29 21:59:23 -07:00
Rhet Turnbull
b611d34d19 Added force_download.py to examples 2020-08-29 21:53:57 -07:00
Daniel M. Drucker
001e474d56 typo fix 2020-08-29 16:58:49 -04:00
Rhet Turnbull
60d96a8f56 Added photoshop:SidecarForExtension to XMP, partial fix for #210 2020-08-25 21:46:07 -07:00
Rhet Turnbull
42e8fba125 Update README.md 2020-08-25 15:21:40 -07:00
Rhet Turnbull
a91617cce4 Updated CHANGELOG.md 2020-08-25 14:25:56 -07:00
Rhet Turnbull
0cc4beaede Fixed DST handling for from_date/to_date, closes #193 (again) 2020-08-25 06:43:06 -07:00
Rhet Turnbull
0f457a4082 Added raw timestamps to PhotoInfo._info 2020-08-24 06:00:57 -07:00
Rhet Turnbull
1f717b0579 Fixed portrait for Catalina/Big Sur; see issue #203 2020-08-23 16:34:23 -07:00
Rhet Turnbull
0cbd005bcd Merge pull request #207 from RhetTbull/issue206
Closes issue #206, adds --touch-file
2020-08-23 11:18:31 -07:00
Rhet Turnbull
1bf7105737 Fixed touch tests 2020-08-23 11:06:01 -07:00
Rhet Turnbull
6e5ea8e013 Fixed touch tests to use correct timezone 2020-08-23 08:37:12 -07:00
Rhet Turnbull
9f64262757 Finished --touch-file, closes #206 2020-08-23 08:27:21 -07:00
Rhet Turnbull
6c11e3fa5b --touch-file now working with --update 2020-08-22 08:12:26 -07:00
Rhet Turnbull
c9c9202205 Working on issue #206 2020-08-21 05:53:52 -07:00
Rhet Turnbull
ebd878a075 Working on issue 206 2020-08-20 06:39:48 -07:00
Rhet Turnbull
2cf3b6bb67 Updated tests/README.md 2020-08-19 06:06:04 -07:00
Rhet Turnbull
beb7970b3b Merge pull request #205 from PabloKohan/touch_files__fix_194
Touch files - fixes #194 -- thanks to @PabloKohan
2020-08-18 06:00:27 -07:00
Rhet Turnbull
2567974f5b Merge pull request #204 from PabloKohan/refactor_export_photo
Refactor/cleanup _export_photo - thanks to @PabloKohan
2020-08-18 05:59:57 -07:00
Pablo 'merKur' Kohan
78d494ff2c Touch file upon image date - Issue #194 2020-08-17 21:58:11 +03:00
Pablo 'merKur' Kohan
eefa1f181f Refactor/cleanup _export_photo 2020-08-17 21:54:47 +03:00
Rhet Turnbull
2bf5fae093 Working on fix for issue #203 2020-08-17 06:32:55 -07:00
Rhet Turnbull
9b13d1e00b Updated README.md 2020-08-16 23:03:00 -07:00
Rhet Turnbull
f2df6f1a12 Updated CHANGELOG.md 2020-08-16 23:01:04 -07:00
Rhet Turnbull
98e417023e Added ImportInfo for Photos 5+ 2020-08-16 22:57:33 -07:00
Rhet Turnbull
360c8d8e1b Update README.md 2020-08-15 15:20:47 -07:00
Rhet Turnbull
868cda8482 Update README.md 2020-08-15 15:14:45 -07:00
Rhet Turnbull
fa149dc7e1 Replaced call to which, closes #171 2020-08-09 18:09:32 -07:00
Rhet Turnbull
7467bbf62b Added contributors to README.md, closes #200 2020-08-09 17:56:40 -07:00
Rhet Turnbull
d2deefff83 Added tests for 10.15.6 2020-08-09 12:14:18 -07:00
Rhet Turnbull
f474dcd2cb Updated CHANGELOG.md 2020-08-09 11:04:59 -07:00
Rhet Turnbull
6acf9acd63 Alpha support for MacOS Big Sur/10.16, see issue #187 2020-08-09 10:59:05 -07:00
Rhet Turnbull
d0ec8620c7 Added py37 2020-08-08 22:04:36 -07:00
Rhet Turnbull
10156e34b5 Updated requirements.txt 2020-08-08 22:03:12 -07:00
Rhet Turnbull
a714ae0af0 Dropped py36 due to datetime.fromisoformat 2020-08-08 21:59:38 -07:00
Rhet Turnbull
fc416ea0b7 Fixed from_date and to_date to be timezone aware, closes #193 2020-08-08 21:03:34 -07:00
Rhet Turnbull
2628c1f2d2 Cleaned up test images 2020-08-08 08:22:28 -07:00
Rhet Turnbull
e482c3915a Added test for valid XMP file, closes #197 2020-08-01 07:58:00 -07:00
Rhet Turnbull
6baeae7ddd Test library updates 2020-08-01 06:55:51 -07:00
Rhet Turnbull
bea770b322 Added write_uuid_to_file.applescript to utils 2020-08-01 06:55:23 -07:00
Rhet Turnbull
840e9937be Added --uuid-from-file to CLI 2020-07-31 19:02:52 -07:00
Rhet Turnbull
002fce8e93 Updated README.md 2020-07-28 22:58:12 -07:00
Rhet Turnbull
ef32b1e9bc Test library updates 2020-07-27 15:31:02 -07:00
Rhet Turnbull
6f29cda99f Initial FaceInfo support for Issue #21 2020-07-27 06:20:04 -07:00
Rhet Turnbull
9fc4f76219 Updated Github Actions to run on PR 2020-07-24 19:03:01 -07:00
Rhet Turnbull
65b84ad345 Updated CHANGELOG.md 2020-07-23 07:20:30 -07:00
Rhet Turnbull
cf4dca10c0 Version bump for bug fix 2020-07-23 07:14:15 -07:00
Rhet Turnbull
27040d1604 Revert "Merge pull request #191 from RhetTbull/revert-190-Fix133"
This reverts commit b7f4b739de, reversing
changes made to da551036f9.
2020-07-23 07:04:42 -07:00
Rhet Turnbull
b91a9828fa Merge pull request #192 from PabloKohan/Fix133
Fix findfiles not to fail on missing/invalid dir
2020-07-23 06:55:47 -07:00
Pablo 'merKur' Kohan
8c10b61e90 Fix findfiles not to fail on missing/invalid dir
Was failing on --dry-run and tests.
Added unit-test.
2020-07-23 15:16:40 +03:00
Rhet Turnbull
b7f4b739de Merge pull request #191 from RhetTbull/revert-190-Fix133
Revert "Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133)"
2020-07-22 22:18:19 -07:00
Rhet Turnbull
f8e62d8f5e Revert "Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133)" 2020-07-22 22:13:39 -07:00
Rhet Turnbull
da551036f9 Merge pull request #190 from PabloKohan/Fix133
Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133)
2020-07-22 21:59:44 -07:00
Pablo 'merKur' Kohan
d52b387a29 Fix FileExistsError when filename differs only in case and export-as-hardlink
When exporting with --export-as-hardlink (and without --overwrite), an
exception is thrown in os.link (FileExistsError: [Errno 17] File exists)

This can happen if filenames differ only in case (on a case-insensitive
filesystem), so the similar filename is not incremented.

This fix uses `findfiles` (to glob in a case-insensitive way) instead of `glob`.
2020-07-22 22:20:48 +03:00
Rhet Turnbull
927e25911e Updated CHANGELOG.md 2020-07-18 16:21:50 -07:00
Rhet Turnbull
6688d1ff64 Updated dependencies, now supports py36, py37, py38 2020-07-18 07:42:52 -07:00
Rhet Turnbull
3526881ec8 Update README.md 2020-07-18 06:54:29 -07:00
Rhet Turnbull
3f19276c5c Implemented PersonInfo, closes #181 2020-07-17 22:06:37 -07:00
Rhet Turnbull
091e7b8f2e Updated CHANGELOG.md 2020-07-06 10:41:18 -07:00
Rhet Turnbull
1ef518cc3e Bug fix for empty albums 2020-07-06 10:35:54 -07:00
Rhet Turnbull
a934b692ab Updated CHANGELOG.md 2020-07-06 10:16:18 -07:00
Rhet Turnbull
9d820a0557 AlbumInfo.photos now returns photos in album sort order 2020-07-06 10:06:11 -07:00
Rhet Turnbull
fcff8ec5f8 Refactored person processing to enable implementation of #181 2020-07-06 00:10:22 -07:00
Rhet Turnbull
dfcbfa725a Updated CHANGELOG.md 2020-07-04 10:17:25 -07:00
Rhet Turnbull
df75a05645 Bug fix for keywords, persons in deleted photos 2020-07-04 09:54:43 -07:00
Rhet Turnbull
80f5989e2c Updated CHANGELOG.md 2020-07-03 12:31:18 -07:00
Rhet Turnbull
8c3af0a4e4 Added height, width, orientation, filesize to json, str) 2020-07-03 12:28:26 -07:00
Rhet Turnbull
4523224276 Updated CHANGELOG.md 2020-07-03 12:04:20 -07:00
Rhet Turnbull
541c390b7b Added height, width, orientation, filesize, closes #163 2020-07-03 11:24:59 -07:00
Rhet Turnbull
6ab0ad7e86 Added GPS location to XMP sidecar, closes #175 2020-07-03 09:04:23 -07:00
Rhet Turnbull
e5755c6144 Updated CHANGELOG.md 2020-06-28 21:54:36 -07:00
Rhet Turnbull
7806e05673 Updated README.md 2020-06-28 21:53:50 -07:00
Rhet Turnbull
bb4bc8fd96 Added --description-template to CLI, closes #166 2020-06-28 20:10:38 -07:00
Rhet Turnbull
59507077ba Updated README.md 2020-06-28 13:50:12 -07:00
Rhet Turnbull
ff0328785f Added expand_inplace to PhotoTemplate.render 2020-06-28 13:46:35 -07:00
2132 changed files with 71326 additions and 6306 deletions

191
.all-contributorsrc Normal file
View File

@@ -0,0 +1,191 @@
{
"projectName": "osxphotos",
"projectOwner": "RhetTbull",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 75,
"badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat)](#contributors)",
"commit": true,
"commitConvention": "none",
"contributors": [
{
"login": "britiscurious",
"name": "britiscurious",
"avatar_url": "https://avatars1.githubusercontent.com/u/25646439?v=4",
"profile": "https://github.com/britiscurious",
"contributions": [
"doc",
"code"
]
},
{
"login": "mwort",
"name": "Michel Wortmann",
"avatar_url": "https://avatars3.githubusercontent.com/u/8170417?v=4",
"profile": "https://github.com/mwort",
"contributions": [
"code"
]
},
{
"login": "PabloKohan",
"name": "Pablo 'merKur' Kohan",
"avatar_url": "https://avatars3.githubusercontent.com/u/8790976?v=4",
"profile": "https://github.com/PabloKohan",
"contributions": [
"code"
]
},
{
"login": "hshore29",
"name": "hshore29",
"avatar_url": "https://avatars2.githubusercontent.com/u/7023497?v=4",
"profile": "https://github.com/hshore29",
"contributions": [
"code"
]
},
{
"login": "dmd",
"name": "Daniel M. Drucker",
"avatar_url": "https://avatars0.githubusercontent.com/u/41439?v=4",
"profile": "http://3e.org/",
"contributions": [
"code"
]
},
{
"login": "jystervinou",
"name": "Jean-Yves Stervinou",
"avatar_url": "https://avatars3.githubusercontent.com/u/132356?v=4",
"profile": "https://github.com/jystervinou",
"contributions": [
"code"
]
},
{
"login": "dethi",
"name": "Thibault Deutsch",
"avatar_url": "https://avatars2.githubusercontent.com/u/1011520?v=4",
"profile": "https://dethi.me/",
"contributions": [
"code"
]
},
{
"login": "grundsch",
"name": "grundsch",
"avatar_url": "https://avatars0.githubusercontent.com/u/3874928?v=4",
"profile": "https://github.com/grundsch",
"contributions": [
"code"
]
},
{
"login": "agprimatic",
"name": "Ag Primatic",
"avatar_url": "https://avatars1.githubusercontent.com/u/4685054?v=4",
"profile": "https://github.com/agprimatic",
"contributions": [
"code"
]
},
{
"login": "hhoeck",
"name": "Horst Höck",
"avatar_url": "https://avatars1.githubusercontent.com/u/6313998?v=4",
"profile": "https://github.com/hhoeck",
"contributions": [
"code"
]
},
{
"login": "jstrine",
"name": "Jonathan Strine",
"avatar_url": "https://avatars1.githubusercontent.com/u/33943447?v=4",
"profile": "https://github.com/jstrine",
"contributions": [
"code"
]
},
{
"login": "finestream",
"name": "finestream",
"avatar_url": "https://avatars1.githubusercontent.com/u/16638513?v=4",
"profile": "https://github.com/finestream",
"contributions": [
"doc"
]
},
{
"login": "synox",
"name": "Aravindo Wingeier",
"avatar_url": "https://avatars2.githubusercontent.com/u/2250964?v=4",
"profile": "https://github.com/synox",
"contributions": [
"doc"
]
},
{
"login": "kradalby",
"name": "Kristoffer Dalby",
"avatar_url": "https://avatars1.githubusercontent.com/u/98431?v=4",
"profile": "https://kradalby.no",
"contributions": [
"code"
]
},
{
"login": "Rott-Apple",
"name": "Rott-Apple",
"avatar_url": "https://avatars1.githubusercontent.com/u/67875570?v=4",
"profile": "https://github.com/Rott-Apple",
"contributions": [
"research"
]
},
{
"login": "narensankar0529",
"name": "narensankar0529",
"avatar_url": "https://avatars3.githubusercontent.com/u/74054766?v=4",
"profile": "https://github.com/narensankar0529",
"contributions": [
"bug",
"userTesting"
]
},
{
"login": "martinhrpi",
"name": "Martin",
"avatar_url": "https://avatars2.githubusercontent.com/u/19407684?v=4",
"profile": "https://github.com/martinhrpi",
"contributions": [
"research",
"userTesting"
]
},
{
"login": "davidjroos",
"name": "davidjroos ",
"avatar_url": "https://avatars.githubusercontent.com/u/15630844?v=4",
"profile": "https://github.com/davidjroos",
"contributions": [
"doc"
]
},
{
"login": "neilpa",
"name": "Neil Pankey",
"avatar_url": "https://avatars.githubusercontent.com/u/42419?v=4",
"profile": "https://neilpa.me",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

@@ -1,15 +1,16 @@
name: Python package
name: Tests
on: [push]
on: [push, pull_request]
jobs:
build:
runs-on: macOS-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
strategy:
max-parallel: 4
matrix:
python-version: [3.8]
python-version: [3.7, 3.8]
steps:
- uses: actions/checkout@v1

16
.gitignore vendored Normal file
View File

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

View File

@@ -4,6 +4,929 @@ 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.40.19](https://github.com/RhetTbull/osxphotos/compare/v0.40.18...v0.40.19)
> 20 February 2021
- Better exception handling for AdjustmentsInfo [`44a1e3e`](https://github.com/RhetTbull/osxphotos/commit/44a1e3e7a7f765bf91c2341e423ec9e5a9e3c1bd)
#### [v0.40.18](https://github.com/RhetTbull/osxphotos/compare/v0.40.17...v0.40.18)
> 20 February 2021
- docs: add neilpa as a contributor [`#383`](https://github.com/RhetTbull/osxphotos/pull/383)
- Added AdjustmentsInfo, #150, #379 [`5ee6aff`](https://github.com/RhetTbull/osxphotos/commit/5ee6affc0525db1975cb5095f62494ef10d92f7e)
- docs: update .all-contributorsrc [skip ci] [`ebac9d0`](https://github.com/RhetTbull/osxphotos/commit/ebac9d0bfb43f59f046aacdd0290d1fcd29a3b5e)
- docs: update README.md [skip ci] [`29716c5`](https://github.com/RhetTbull/osxphotos/commit/29716c52726a4e699c03d43ecc67db57f55b36f8)
- Version bump [`fbe8229`](https://github.com/RhetTbull/osxphotos/commit/fbe822910370652975ab83b82344169df4c3027c)
#### [v0.40.17](https://github.com/RhetTbull/osxphotos/compare/v0.40.16...v0.40.17)
> 18 February 2021
- Updated docs for --ignore-signature, #286 [`e5f1c29`](https://github.com/RhetTbull/osxphotos/commit/e5f1c299742fcfa0a855a33df7b266aa2c39e48b)
- Added depth_state to _info [`b3a7869`](https://github.com/RhetTbull/osxphotos/commit/b3a7869bd3cc13e40cb3f68ff8caf12edda9a49c)
#### [v0.40.16](https://github.com/RhetTbull/osxphotos/compare/v0.40.14...v0.40.16)
> 14 February 2021
- Write description to ITPC:CaptionAbstract (#380) [`4b7a53f`](https://github.com/RhetTbull/osxphotos/commit/4b7a53faa8d7ff2e941e7653554f61bcbd416fc9)
- Removed orientation from XMP, #378 [`70848e1`](https://github.com/RhetTbull/osxphotos/commit/70848e1ff6def928b052271b47c1697c23a8c73f)
- Added image orientation bug to Known Bugs [`1316866`](https://github.com/RhetTbull/osxphotos/commit/1316866dc47486ac61db8903d2d7d006f2598a77)
#### [v0.40.14](https://github.com/RhetTbull/osxphotos/compare/v0.40.13...v0.40.14)
> 12 February 2021
- Fix for issue #366, --jpeg-ext, --convert-to-jpeg bug [`3027350`](https://github.com/RhetTbull/osxphotos/commit/30273509d40a270d2610b662ed9238449350064c)
- Added test for #374 [`2691902`](https://github.com/RhetTbull/osxphotos/commit/2691902d5c7a4f4f81e3a9b36fd560ff0a07aec1)
#### [v0.40.13](https://github.com/RhetTbull/osxphotos/compare/v0.40.12...v0.40.13)
> 10 February 2021
- Bug fix for --jpeg-ext, #374 [`da47821`](https://github.com/RhetTbull/osxphotos/commit/da47821fae7ee7b2d6d89f5542e729e01d3338df)
#### [v0.40.12](https://github.com/RhetTbull/osxphotos/compare/v0.40.11...v0.40.12)
> 9 February 2021
- Fixed --exiftool-option, #369, for real this time [`857e3db`](https://github.com/RhetTbull/osxphotos/commit/857e3db6ccce810d682cd4632ac9bc8448c4f86b)
#### [v0.40.11](https://github.com/RhetTbull/osxphotos/compare/v0.40.10...v0.40.11)
> 9 February 2021
- Fixed --exiftool-option, #369 [`198adda`](https://github.com/RhetTbull/osxphotos/commit/198addaa07a86ac5b0fd82787fdffff0a0fc19c6)
#### [v0.40.10](https://github.com/RhetTbull/osxphotos/compare/v0.40.9...v0.40.10)
> 7 February 2021
- Fix for issue #366 [`5c3360f`](https://github.com/RhetTbull/osxphotos/commit/5c3360f29d52df2f804c70f37a2ca9a3f102d93c)
#### [v0.40.9](https://github.com/RhetTbull/osxphotos/compare/v0.40.8...v0.40.9)
> 7 February 2021
- Fixed unnecessary warning for long keywords, issue #365 [`f8616ac`](https://github.com/RhetTbull/osxphotos/commit/f8616acf167b5e73ab3e4b68dcfbf578230c330d)
#### [v0.40.8](https://github.com/RhetTbull/osxphotos/compare/v0.40.7...v0.40.8)
> 4 February 2021
- Implemented --in-album, --not-in-album, issue #364 [`addd952`](https://github.com/RhetTbull/osxphotos/commit/addd952aa315007852945a352b2c7c451ba5f21a)
- Updated docs [`7fa5fba`](https://github.com/RhetTbull/osxphotos/commit/7fa5fbaa5b7c9aa1412eceef56e068dc044c91e0)
- Updated docs Makefile [skip ci] [`683dfe7`](https://github.com/RhetTbull/osxphotos/commit/683dfe7f3ffd235659b58f403562ce2d51123cfb)
#### [v0.40.7](https://github.com/RhetTbull/osxphotos/compare/v0.40.6...v0.40.7)
> 3 February 2021
- Bump bleach from 3.1.4 to 3.3.0 [`#362`](https://github.com/RhetTbull/osxphotos/pull/362)
- Fixed XMP template for issue #361 [`43af4d2`](https://github.com/RhetTbull/osxphotos/commit/43af4d205a7264e530bc2b2789d297be633391e1)
- Updated sidecar test data [`591f9bc`](https://github.com/RhetTbull/osxphotos/commit/591f9bcc62720f7eddebba3b3dcff265907550dd)
- Added tests for --only-new, #358 [`adc4b05`](https://github.com/RhetTbull/osxphotos/commit/adc4b056029794faddd464d22022a2a17298a924)
- Updated tests for ExportDB, #358 [`48d2223`](https://github.com/RhetTbull/osxphotos/commit/48d2223edde4850830cc6a3f9776ce08f81a6636)
- Added 11.2 to tested versions, #360 [`2284598`](https://github.com/RhetTbull/osxphotos/commit/2284598a24f63232c01dcf27b9982002123834ca)
#### [v0.40.6](https://github.com/RhetTbull/osxphotos/compare/v0.40.5...v0.40.6)
> 2 February 2021
- Add @davidjroos as a contributor [`8dbedef`](https://github.com/RhetTbull/osxphotos/commit/8dbedef1874882815afb4a885184249aae73bf9f)
- Fixed documentation, #359 [`77371b6`](https://github.com/RhetTbull/osxphotos/commit/77371b6e5d8a9b8662b7b7d540378beb897f6988)
#### [v0.40.5](https://github.com/RhetTbull/osxphotos/compare/v0.40.3...v0.40.5)
> 1 February 2021
- Restructured docs [`3a4a8bd`](https://github.com/RhetTbull/osxphotos/commit/3a4a8bdb0bdd995c937e0a15f5d8f1685b73407f)
- Refactored __main__, added sphinx docs [`51f6958`](https://github.com/RhetTbull/osxphotos/commit/51f69585be60d12f912ba08f138b9c1f74481dbd)
- Implemented --only-new, #358 [`5c093c4`](https://github.com/RhetTbull/osxphotos/commit/5c093c43528193ed1704ed4ef1b8d841a95a81cf)
#### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3)
> 23 January 2021
- Fix for issue #348 [`5a69636`](https://github.com/RhetTbull/osxphotos/commit/5a696366fa37fc6eafebb64fa154eee7624819a7)
- Fixed sidecar test data [`ebe2fc5`](https://github.com/RhetTbull/osxphotos/commit/ebe2fc544d3c89050924da331921dc6f6fa5d79a)
- Version bump [`4e47de7`](https://github.com/RhetTbull/osxphotos/commit/4e47de7589f9df54ea1802275eabf7f9b5d943dd)
#### [v0.40.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.25...v0.40.2)
> 23 January 2021
- Fix for issue #353, #354 [`a287bfb`](https://github.com/RhetTbull/osxphotos/commit/a287bfb41f0ee1ab19db39e6f3eb7183093599a9)
- Updated test data [`6d55851`](https://github.com/RhetTbull/osxphotos/commit/6d55851f75cc1818cbbdd0ab356dc9b5cc078b68)
- Updated docs [`0904a6e`](https://github.com/RhetTbull/osxphotos/commit/0904a6e44db209520e3b8807229487770461c62f)
#### [v0.39.25](https://github.com/RhetTbull/osxphotos/compare/v0.39.24...v0.39.25)
> 19 January 2021
- face region fixes for mirrored images [`bdc4b23`](https://github.com/RhetTbull/osxphotos/commit/bdc4b23f42f5636834d1246234fa0f88089c71a4)
#### [v0.39.24](https://github.com/RhetTbull/osxphotos/compare/v0.39.23...v0.39.24)
> 19 January 2021
- Fixed face regions for exif orientation 6, 8 [`86018d5`](https://github.com/RhetTbull/osxphotos/commit/86018d5cc0d964760fd64047ce52f1f54fc28dc0)
- version bump [`2f86625`](https://github.com/RhetTbull/osxphotos/commit/2f866256adfdf39244241ca6bbcc7a8d072555b9)
#### [v0.39.23](https://github.com/RhetTbull/osxphotos/compare/v0.39.22...v0.39.23)
> 18 January 2021
- Fixed face region orientation [`875f79b`](https://github.com/RhetTbull/osxphotos/commit/875f79b92d9510e59fe8ca0aa21a42abc7600f70)
- Updated documentation for new face region properties [`3a110bb`](https://github.com/RhetTbull/osxphotos/commit/3a110bb6d3d23d1c9fd8612b4201144046fed567)
#### [v0.39.22](https://github.com/RhetTbull/osxphotos/compare/v0.39.21...v0.39.22)
> 18 January 2021
- Beta fix for Digikam reading XMP [`3799594`](https://github.com/RhetTbull/osxphotos/commit/379959447373f951ffca372598ea8f1d5834fe52)
- Add @martinhrpi as a contributor [`db43017`](https://github.com/RhetTbull/osxphotos/commit/db430173b59732f944ca52b53c928370684580df)
#### [v0.39.21](https://github.com/RhetTbull/osxphotos/compare/v0.39.20...v0.39.21)
> 18 January 2021
- Added beta support for face regions in xmp [`2773ff7`](https://github.com/RhetTbull/osxphotos/commit/2773ff73815ef4667f88a45b016539e490d31769)
- Fixed osxphotos.spec datas [`f58f8dd`](https://github.com/RhetTbull/osxphotos/commit/f58f8dd804f432d07048b98e5dcedca57fec0a5e)
#### [v0.39.20](https://github.com/RhetTbull/osxphotos/compare/v0.39.19...v0.39.20)
> 16 January 2021
- Added isreference property and --is-reference, #321 [`651ed50`](https://github.com/RhetTbull/osxphotos/commit/651ed50a076bd3685c7d7a568e53960363d5c30b)
- version bump [`9c18cee`](https://github.com/RhetTbull/osxphotos/commit/9c18cee37e961d2e1059490ad1dbe4e45c501002)
#### [v0.39.19](https://github.com/RhetTbull/osxphotos/compare/v0.39.18...v0.39.19)
> 15 January 2021
- Added retry to use_photos_export, issue #351 [`ddce731`](https://github.com/RhetTbull/osxphotos/commit/ddce731a5d354e833d56a64d06cdbc39711f693e)
#### [v0.39.18](https://github.com/RhetTbull/osxphotos/compare/v0.39.17...v0.39.18)
> 15 January 2021
- Fixed XMP sidecars to conform with exiftool format, #349, #350 [`1fd0fe5`](https://github.com/RhetTbull/osxphotos/commit/1fd0fe5ea477ccea43c78086af440bd32dc702d8)
- Added update_readme.py to auto-build README [`fd5976b`](https://github.com/RhetTbull/osxphotos/commit/fd5976b75c79a3d205db2e8132c388de95632b77)
- Added modified.strftime template, refactored test_template.py [`088476c`](https://github.com/RhetTbull/osxphotos/commit/088476c59126c6d6fe75551ff122e81aababf818)
#### [v0.39.17](https://github.com/RhetTbull/osxphotos/compare/v0.39.16...v0.39.17)
> 12 January 2021
- Fixed test for M1, added about command, closes #315 [`#315`](https://github.com/RhetTbull/osxphotos/issues/315)
- Fixed time zone for tests [`165f9b0`](https://github.com/RhetTbull/osxphotos/commit/165f9b08f5056d1f0b2ca7c74cec84d42b635663)
- Add @narensankar0529 as a contributor [`039118c`](https://github.com/RhetTbull/osxphotos/commit/039118c1aaa217f46354b351ea36b0729e3e1c35)
- Update @narensankar0529 as a contributor [`61f649e`](https://github.com/RhetTbull/osxphotos/commit/61f649e59d53a3e3011602476b72cc64951d38c0)
#### [v0.39.16](https://github.com/RhetTbull/osxphotos/compare/v0.39.15...v0.39.16)
> 12 January 2021
- Added version check for M1 macs [`27f779b`](https://github.com/RhetTbull/osxphotos/commit/27f779b16c850cdbda2691e5fae8cd14405653b3)
#### [v0.39.15](https://github.com/RhetTbull/osxphotos/compare/v0.39.13...v0.39.15)
> 11 January 2021
- Completed implementation of --jpeg-ext, fixed --dry-run, closes #330, #346 [`#330`](https://github.com/RhetTbull/osxphotos/issues/330)
- Added --jpeg-ext, implements #330 [`55c088e`](https://github.com/RhetTbull/osxphotos/commit/55c088eea2ddecb14e362221da9e2a7c0f403780)
#### [v0.39.13](https://github.com/RhetTbull/osxphotos/compare/v0.39.12...v0.39.13)
> 10 January 2021
- Fixed leaky memory in PhotoKit, issue #276 [`db1947d`](https://github.com/RhetTbull/osxphotos/commit/db1947dd1e3d47a487eeb68a5ceb5f7098f1df10)
#### [v0.39.12](https://github.com/RhetTbull/osxphotos/compare/v0.39.11...v0.39.12)
> 9 January 2021
- Force cleanup of objects in write_jpeg (fix memory leak) [`#344`](https://github.com/RhetTbull/osxphotos/pull/344)
- doc: Recorded screencast and updated of readme [skip ci] [`#328`](https://github.com/RhetTbull/osxphotos/pull/328)
- Added PhotoInfo.visible, PhotoInfo.date_trashed, closes #333, #334 [`#333`](https://github.com/RhetTbull/osxphotos/issues/333)
- Force cleanup of objects with autorelease pool [`b67f11a`](https://github.com/RhetTbull/osxphotos/commit/b67f11a3bb95c08a39a185b6d884092870e949f2)
- Add @Rott-Apple as a contributor [`71cb015`](https://github.com/RhetTbull/osxphotos/commit/71cb01572d2d946df18dd7b36f95b2f2e5b48f86)
- Updated README [skip ci] [`804e13e`](https://github.com/RhetTbull/osxphotos/commit/804e13efff921ab51b996493d659b32102807a8a)
#### [v0.39.11](https://github.com/RhetTbull/osxphotos/compare/v0.39.10...v0.39.11)
> 8 January 2021
- All contributors/add kradalby [`#343`](https://github.com/RhetTbull/osxphotos/pull/343)
- Ensure keyword list only contains strings, @all-contributors please add @kradalby for code [`#342`](https://github.com/RhetTbull/osxphotos/pull/342)
- Added README.rst, closes #331 [`#331`](https://github.com/RhetTbull/osxphotos/issues/331)
- Updated tests workflow badge link [`a7678df`](https://github.com/RhetTbull/osxphotos/commit/a7678df3974ff539050f5acb4c94817f525dcd56)
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`e6f45f5`](https://github.com/RhetTbull/osxphotos/commit/e6f45f59491d9e805e227af8cbf8ac08ff99fdf0)
- Renamed workflow to tests [`f8468c6`](https://github.com/RhetTbull/osxphotos/commit/f8468c63fda930216f73ad5aa8c4aa92edf1adf2)
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`5de9d4f`](https://github.com/RhetTbull/osxphotos/commit/5de9d4f90c1102c4fb0099befd6142180f32df3f)
- Ensure merge_exif_keywords are str not int [`123ebb2`](https://github.com/RhetTbull/osxphotos/commit/123ebb2cb752bb94291ac2b77e4a327cee996df1)
#### [v0.39.10](https://github.com/RhetTbull/osxphotos/compare/v0.39.9...v0.39.10)
> 6 January 2021
- Refactored ExportResults [`568d1b3`](https://github.com/RhetTbull/osxphotos/commit/568d1b36a631df33317dc00f27126b507c90bf51)
- Improved handling of deleted photos, #332 [`792247b`](https://github.com/RhetTbull/osxphotos/commit/792247b51cc2263221ba8c2e741d2ec454c75ca8)
- Added error_str to ExportResults [`d78097c`](https://github.com/RhetTbull/osxphotos/commit/d78097ccc0686680baf5fffa91f9e082e44b576e)
#### [v0.39.9](https://github.com/RhetTbull/osxphotos/compare/v0.39.8...v0.39.9)
> 4 January 2021
- Added test for Big Sur 16.0.1 database changes [`7deac58`](https://github.com/RhetTbull/osxphotos/commit/7deac581b1f1fb3dc59885b6e1ab9a63b382408d)
- Create terminalizer-demo.yml [`5dc2eea`](https://github.com/RhetTbull/osxphotos/commit/5dc2eeaf9a7265873c81db23bbc86d3023189a26)
- doc: Recorded screencast and updated of readme [`658e8ac`](https://github.com/RhetTbull/osxphotos/commit/658e8ac096d141fce48483dbfc1426bea317d806)
#### [v0.39.8](https://github.com/RhetTbull/osxphotos/compare/v0.39.7...v0.39.8)
> 3 January 2021
- Updated README, version [`b93d682`](https://github.com/RhetTbull/osxphotos/commit/b93d6822ac5366c57d9142cba9b809b4ab99ad98)
#### [v0.39.7](https://github.com/RhetTbull/osxphotos/compare/v0.39.6...v0.39.7)
> 3 January 2021
- doc: start with examples before the export reference, thanks to @synox [`#327`](https://github.com/RhetTbull/osxphotos/pull/327)
- Added tag_groups arg to ExifTool.asdict(), issue #324 [`2480f2a`](https://github.com/RhetTbull/osxphotos/commit/2480f2a325dbb09689f8c417618b7b9e976bfcb9)
- doc: start with examples before the export reference [`7c7bf1b`](https://github.com/RhetTbull/osxphotos/commit/7c7bf1be6b6382a995a4e17906adfd8720d0a1c3)
- Updated dependencies in README.md [`b1cab32`](https://github.com/RhetTbull/osxphotos/commit/b1cab32ff4c7b65ae4c9a5a9a11c175dbd487c0a)
- remove extra spaces [`a59bb5b`](https://github.com/RhetTbull/osxphotos/commit/a59bb5b02f10fa554dae346a7271be37f50d8bcc)
- Adding back dependency https://github.com/RhetTbull/PhotoScript) [`7c8bfc8`](https://github.com/RhetTbull/osxphotos/commit/7c8bfc811ab3a93dabadf1655f7d0e217d6c7b01)
#### [v0.39.6](https://github.com/RhetTbull/osxphotos/compare/v0.39.5...v0.39.6)
> 3 January 2021
- Make readme easier for beginners, thanks to @synox [`#326`](https://github.com/RhetTbull/osxphotos/pull/326)
- doc simplify readme [`02ef0f9`](https://github.com/RhetTbull/osxphotos/commit/02ef0f9a254e83a3729a09cea1ae523407074896)
- Added exception handling/capture for convert-to-jpeg, issue #322 [`05f111a`](https://github.com/RhetTbull/osxphotos/commit/05f111a287e882ed6b451a550a87753501316aba)
- Add @synox as a contributor [`83915c6`](https://github.com/RhetTbull/osxphotos/commit/83915c65abb880036f80ebd830eb1e34292f9599)
#### [v0.39.5](https://github.com/RhetTbull/osxphotos/compare/v0.39.4...v0.39.5)
> 3 January 2021
- Cleanup up the readme [`38842ff`](https://github.com/RhetTbull/osxphotos/commit/38842ff9249e6f5b3069a88a759c8df97ddce51c)
#### [v0.39.4](https://github.com/RhetTbull/osxphotos/compare/v0.39.3...v0.39.4)
> 3 January 2021
- Implemented text replacement for templates, issue #316 [`478715a`](https://github.com/RhetTbull/osxphotos/commit/478715a363f5009e4a38148e832bf0ad3c4cc4f8)
#### [v0.39.3](https://github.com/RhetTbull/osxphotos/compare/v0.39.2...v0.39.3)
> 31 December 2020
- Fixed modified template to use creation time if no modificationd date, issue #312 [`2f57abd`](https://github.com/RhetTbull/osxphotos/commit/2f57abd23cabe57bcf667a1713c37689b330a702)
#### [v0.39.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.1...v0.39.2)
> 31 December 2020
- Added --xattr-template, closes #242 [`#242`](https://github.com/RhetTbull/osxphotos/issues/242)
#### [v0.39.1](https://github.com/RhetTbull/osxphotos/compare/v0.39.0...v0.39.1)
> 31 December 2020
- Fixed --exiftool-path bug, issue #311, #313 [`3394c52`](https://github.com/RhetTbull/osxphotos/commit/3394c527682d8fdd2f20f4f778d802dab86b6372)
#### [v0.39.0](https://github.com/RhetTbull/osxphotos/compare/v0.38.22...v0.39.0)
> 30 December 2020
- Added Finder tags, partial implementation for issue #242 [`#310`](https://github.com/RhetTbull/osxphotos/pull/310)
- Added tests for Finder tags [`29e4245`](https://github.com/RhetTbull/osxphotos/commit/29e424575a522ae03efe5a140be46bfd0a1346c5)
- Initial implementation for Finder tags [`5885b23`](https://github.com/RhetTbull/osxphotos/commit/5885b23d3249cf91953092a6b1ce967da2667e29)
- Updated README for finder tags [`f25a299`](https://github.com/RhetTbull/osxphotos/commit/f25a2993097ad7b2b8ab2d1c787db58c0d799a41)
- Updated requirements.txt [`ea373c4`](https://github.com/RhetTbull/osxphotos/commit/ea373c4197ce1cce00e89157fe560d1366f7e764)
#### [v0.38.22](https://github.com/RhetTbull/osxphotos/compare/v0.38.21...v0.38.22)
> 30 December 2020
- Fixed --exiftool-path bug, issue #308 [`5dccdf7`](https://github.com/RhetTbull/osxphotos/commit/5dccdf7750611c78de5356bb02f6023d4fc382c5)
#### [v0.38.21](https://github.com/RhetTbull/osxphotos/compare/v0.38.20...v0.38.21)
> 29 December 2020
- Fixed --exiftool-path to work with --exiftool-merge-keywords/persons [`3872e7a`](https://github.com/RhetTbull/osxphotos/commit/3872e7ae649f42d849de472a7dbf78a241d54407)
#### [v0.38.20](https://github.com/RhetTbull/osxphotos/compare/v0.38.19...v0.38.20)
> 29 December 2020
- Added --exiftool-path to CLI [`4897fc4`](https://github.com/RhetTbull/osxphotos/commit/4897fc4b05cc7a3bea314f9cce8a2163bf3922b2)
#### [v0.38.19](https://github.com/RhetTbull/osxphotos/compare/v0.38.18...v0.38.19)
> 29 December 2020
- Added exiftool signature to JSON output, issue #303 [`fa58af8`](https://github.com/RhetTbull/osxphotos/commit/fa58af8b883da11fdfa723d2da75a600d927d46e)
#### [v0.38.18](https://github.com/RhetTbull/osxphotos/compare/v0.38.17...v0.38.18)
> 28 December 2020
- Added --exiftool-merge-keywords/persons, issue #299, #292 [`b1cb99f`](https://github.com/RhetTbull/osxphotos/commit/b1cb99f83f55128a314d265d4588134cb79026c6)
#### [v0.38.17](https://github.com/RhetTbull/osxphotos/compare/v0.38.16...v0.38.17)
> 28 December 2020
- Added --sidecar-drop-ext, issue #291 [`dce002c`](https://github.com/RhetTbull/osxphotos/commit/dce002cdfe12fa5fa4ada4d5097828a5375c2ecd)
- Updated Template Substitution table [`7bd189e`](https://github.com/RhetTbull/osxphotos/commit/7bd189e9b22a2ad5a8a80deb7cb93c61be37c771)
#### [v0.38.16](https://github.com/RhetTbull/osxphotos/compare/v0.38.15...v0.38.16)
> 28 December 2020
- Added searchinfo templates, issue #302 [`0d086bf`](https://github.com/RhetTbull/osxphotos/commit/0d086bf85102ce78b3111c64bfa88673fbc19559)
#### [v0.38.15](https://github.com/RhetTbull/osxphotos/compare/v0.38.14...v0.38.15)
> 28 December 2020
- Added --sidecar exiftool, issue #303 [`d833c14`](https://github.com/RhetTbull/osxphotos/commit/d833c14ef4b3f9375a85034cf0fb0f85a68cabb4)
- Refactored sidecar code [`ade98fc`](https://github.com/RhetTbull/osxphotos/commit/ade98fc15051684bfb54d0199d9c370481b70dcc)
- Refactored export2 to use sidecar bit field [`0d66759`](https://github.com/RhetTbull/osxphotos/commit/0d66759b1c200f1ecda202e28c259f88fd3db599)
#### [v0.38.14](https://github.com/RhetTbull/osxphotos/compare/v0.38.13...v0.38.14)
> 27 December 2020
- Bug fix for --description-template, issue #304 [`4cc40d2`](https://github.com/RhetTbull/osxphotos/commit/4cc40d24cfb11ef8668c5d3c3bab40371fdd0436)
#### [v0.38.13](https://github.com/RhetTbull/osxphotos/compare/v0.38.12...v0.38.13)
> 27 December 2020
- Set XMP:Subject to match Keywords, issue #302 [`75888cd`](https://github.com/RhetTbull/osxphotos/commit/75888cd6633d3f0180d24fef4f6776986a136f0f)
#### [v0.38.12](https://github.com/RhetTbull/osxphotos/compare/v0.38.11...v0.38.12)
> 26 December 2020
- Fixed city/sub-locality for SearchInfo [`f9f699b`](https://github.com/RhetTbull/osxphotos/commit/f9f699ba3500d58494f955d4e5d8118e336e6a2c)
#### [v0.38.11](https://github.com/RhetTbull/osxphotos/compare/v0.38.9...v0.38.11)
> 26 December 2020
- Exposed SearchInfo, closes #121 [`#121`](https://github.com/RhetTbull/osxphotos/issues/121)
- Added version to --verbose, closes #297 [`#297`](https://github.com/RhetTbull/osxphotos/issues/297)
- Added --exportdb [`2a49255`](https://github.com/RhetTbull/osxphotos/commit/2a49255277d3c6bd3b0d5f8288afd7de7dab0320)
- Updated README.md [`f469ccc`](https://github.com/RhetTbull/osxphotos/commit/f469cccc4b4561db7611c3e9abf5aefc3ab0f648)
- Fixed help text [`f3b7134`](https://github.com/RhetTbull/osxphotos/commit/f3b7134af1e3d07fb956eaccccd9d60bd075d3bf)
#### [v0.38.9](https://github.com/RhetTbull/osxphotos/compare/v0.38.8...v0.38.9)
> 21 December 2020
- Added --exiftool-option to CLI, closes #298 [`#298`](https://github.com/RhetTbull/osxphotos/issues/298)
#### [v0.38.8](https://github.com/RhetTbull/osxphotos/compare/v0.38.7...v0.38.8)
> 21 December 2020
- remove duplicate keywords with --exiftool and --sidecar, closes #294 [`#294`](https://github.com/RhetTbull/osxphotos/issues/294)
#### [v0.38.7](https://github.com/RhetTbull/osxphotos/compare/v0.38.6...v0.38.7)
> 21 December 2020
- Added better exiftool error handling, closes #300 [`#300`](https://github.com/RhetTbull/osxphotos/issues/300)
- README.md updates for tested versions [`8d1ccda`](https://github.com/RhetTbull/osxphotos/commit/8d1ccda0c897f84342caf612c1070d78bff421f5)
- version bump [`ef94933`](https://github.com/RhetTbull/osxphotos/commit/ef94933dd87b9ad2a516163ca50a36753dacd55a)
#### [v0.38.6](https://github.com/RhetTbull/osxphotos/compare/v0.38.5...v0.38.6)
> 18 December 2020
- Documentation fix for #293. Thanks to @finestream [`#295`](https://github.com/RhetTbull/osxphotos/pull/295)
- Added additional test cases for #286, --ignore-signature [`880a9b6`](https://github.com/RhetTbull/osxphotos/commit/880a9b67a14787ef23ae68ad3164d7eda1af16ec)
- Add @finestream as a contributor [`ad860b1`](https://github.com/RhetTbull/osxphotos/commit/ad860b1500dffd846322e05562ba4f2019cd1017)
- Fixed issue #296 [`a7c688c`](https://github.com/RhetTbull/osxphotos/commit/a7c688cfc2221833e0252d71bbe596eee5f9a6e8)
- Updated README.md [`d40b16a`](https://github.com/RhetTbull/osxphotos/commit/d40b16a456c64014674505b7c715c80b977da76a)
- Version bump [`4678f15`](https://github.com/RhetTbull/osxphotos/commit/4678f15bc86b5dedcb73c73f40e5fe11c1b51fa0)
#### [v0.38.5](https://github.com/RhetTbull/osxphotos/compare/v0.38.4...v0.38.5)
> 17 December 2020
- Patch 1 [`#1`](https://github.com/RhetTbull/osxphotos/pull/1)
- Implemented --ignore-signature, issue #286 [`e394d8e`](https://github.com/RhetTbull/osxphotos/commit/e394d8e6be7607a1668029bcb37ccb30a4fa792f)
- Update __main__.py [`e097f3a`](https://github.com/RhetTbull/osxphotos/commit/e097f3aad546b5be5eabab529bd2c35ce3056876)
- Update README.md [`4f64eeb`](https://github.com/RhetTbull/osxphotos/commit/4f64eeb996d43953eb90618465d2bd046282c4bb)
- Update README.md [`3155045`](https://github.com/RhetTbull/osxphotos/commit/3155045ec87d83285f2e66210559f4be0a10e3a2)
#### [v0.38.4](https://github.com/RhetTbull/osxphotos/compare/v0.38.3...v0.38.4)
> 14 December 2020
- Fix for issue #263 [`d5730dd`](https://github.com/RhetTbull/osxphotos/commit/d5730dd8ae92bc819b61ab4df9b10ae64e23569f)
#### [v0.38.3](https://github.com/RhetTbull/osxphotos/compare/v0.38.2...v0.38.3)
> 13 December 2020
- Fix for QuickTime date/time, issue #282 [`d8593a0`](https://github.com/RhetTbull/osxphotos/commit/d8593a01e210a0b914d5668ad5f70976fc43b217)
#### [v0.38.2](https://github.com/RhetTbull/osxphotos/compare/v0.38.0...v0.38.2)
> 12 December 2020
- Added --save-config, --load-config [`#290`](https://github.com/RhetTbull/osxphotos/pull/290)
- removed extended_attributes reference [`6559c4d`](https://github.com/RhetTbull/osxphotos/commit/6559c4d8f64ad41df925182f9f24f6f67eecd1df)
- This is why I never use branches [`baf45cc`](https://github.com/RhetTbull/osxphotos/commit/baf45ccd2aa24858bb1a8f95ef798121ee80af30)
- Version bump [`aca85ee`](https://github.com/RhetTbull/osxphotos/commit/aca85ee2aa01fcdece0224332584082280a3f62c)
- Merge branch 'master' into save_config [`9584a9c`](https://github.com/RhetTbull/osxphotos/commit/9584a9ccc56ac8c6dc5eb96019adf9224f436690)
- Added tests for configoptions.py [`0262e0d`](https://github.com/RhetTbull/osxphotos/commit/0262e0d97e06ee36786b4491efa178608afb5de5)
#### [v0.38.0](https://github.com/RhetTbull/osxphotos/compare/v0.37.7...v0.38.0)
> 11 December 2020
- Initial implementation of configoptions for --save-config, --load-config [`22355fd`](https://github.com/RhetTbull/osxphotos/commit/22355fd44609f42e412c580dfc9e5e0b7cf6c464)
- Refactoring of save-config/load-config code [`37b1e5c`](https://github.com/RhetTbull/osxphotos/commit/37b1e5ca472e9679301fa96d2b7fdd8c4ad438b2)
- Refactored FileUtil to use copy-on-write no APFS, issue #287 [`ec4b53e`](https://github.com/RhetTbull/osxphotos/commit/ec4b53ed9dd2bc1e6b71349efdaf0b81c6d797e5)
#### [v0.37.7](https://github.com/RhetTbull/osxphotos/compare/v0.37.6...v0.37.7)
> 7 December 2020
- Fix for issue #262 [`11f563a`](https://github.com/RhetTbull/osxphotos/commit/11f563a47926798295e24872bc0efcaaba35906f)
#### [v0.37.6](https://github.com/RhetTbull/osxphotos/compare/v0.37.5...v0.37.6)
> 6 December 2020
- Added --cleanup, issue #262 [`e5d6f21`](https://github.com/RhetTbull/osxphotos/commit/e5d6f21d8e85f092fd0cc06ea4a0eaa12834c011)
#### [v0.37.5](https://github.com/RhetTbull/osxphotos/compare/v0.37.4...v0.37.5)
> 5 December 2020
- Fix for issue #257, #275 [`1b6a03a`](https://github.com/RhetTbull/osxphotos/commit/1b6a03a9f8c76cb5e50caab6eb138a56ccd841dd)
#### [v0.37.4](https://github.com/RhetTbull/osxphotos/compare/v0.37.3...v0.37.4)
> 5 December 2020
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`69cd236`](https://github.com/RhetTbull/osxphotos/commit/69cd2367122a3a86044df2845e706d3510bdf2c1)
- Implement fix for issue #282, QuickTime metadata [`4cce9d4`](https://github.com/RhetTbull/osxphotos/commit/4cce9d4939a00ad2d265a510a2c6f0c8e6a8c655)
- Implement fix for issue #282, QuickTime metadata [`cfb07cb`](https://github.com/RhetTbull/osxphotos/commit/cfb07cbfafaac493f6221be482c432812534ddfa)
#### [v0.37.3](https://github.com/RhetTbull/osxphotos/compare/v0.37.2...v0.37.3)
> 30 November 2020
- Removed --use-photokit authorization check, issue 278 [`ed3a971`](https://github.com/RhetTbull/osxphotos/commit/ed3a9711dc0805aed1aacc30e01eeb9c1077d9e1)
#### [v0.37.2](https://github.com/RhetTbull/osxphotos/compare/v0.37.1...v0.37.2)
> 29 November 2020
- Catch errors in export_photo [`d9dcf09`](https://github.com/RhetTbull/osxphotos/commit/d9dcf0917a541725d1e472e7f918733e4e2613d0)
- Added --missing to export, see issue #277 [`25eacc7`](https://github.com/RhetTbull/osxphotos/commit/25eacc7caddd6721232b3f77a02532fcd35f7836)
#### [v0.37.1](https://github.com/RhetTbull/osxphotos/compare/v0.37.0...v0.37.1)
> 28 November 2020
- Added --report option to CLI, implements #253 [`d22eaf3`](https://github.com/RhetTbull/osxphotos/commit/d22eaf39edc8b0b489b011d6d21345dcedcc8dff)
- Updated template values [`af827d7`](https://github.com/RhetTbull/osxphotos/commit/af827d7a5769f41579d300a7cc511251d86b7eed)
#### [v0.37.0](https://github.com/RhetTbull/osxphotos/compare/v0.36.25...v0.37.0)
> 28 November 2020
- Added {exiftool} template, implements issue #259 [`48acb42`](https://github.com/RhetTbull/osxphotos/commit/48acb42631226a71bfc636eea2d3151f1b7165f4)
#### [v0.36.25](https://github.com/RhetTbull/osxphotos/compare/v0.36.24...v0.36.25)
> 27 November 2020
- Added --original-suffix for issue #263 [`399d432`](https://github.com/RhetTbull/osxphotos/commit/399d432a66354b9c235f30d10c6985fbde1b7e4f)
#### [v0.36.24](https://github.com/RhetTbull/osxphotos/compare/v0.36.23...v0.36.24)
> 26 November 2020
- Initial implementation for issue #265 [`382fca3`](https://github.com/RhetTbull/osxphotos/commit/382fca3f92a3c251c12426dd0dc6d7dc21b691cf)
- More work on issue #265 [`d5a9f76`](https://github.com/RhetTbull/osxphotos/commit/d5a9f767199d25ebd9d5925d05ee39ea7e51ac26)
- Simplified sidecar table in export_db [`0632a97`](https://github.com/RhetTbull/osxphotos/commit/0632a97f55af67c7e5265b0d3283155c7c087e89)
#### [v0.36.23](https://github.com/RhetTbull/osxphotos/compare/v0.36.22...v0.36.23)
> 26 November 2020
- Fix for missing original_filename, issue #267 [`fa33218`](https://github.com/RhetTbull/osxphotos/commit/fa332186ab3cdbe1bfd6496ff29b652ef984a5f8)
- version bump [`b5195f9`](https://github.com/RhetTbull/osxphotos/commit/b5195f9d2b81cf6737b65e3cd3793ea9b0da13eb)
- Updated test [`aa2ebf5`](https://github.com/RhetTbull/osxphotos/commit/aa2ebf55bb50eec14f86a532334b376e407f4bbc)
#### [v0.36.22](https://github.com/RhetTbull/osxphotos/compare/v0.36.21...v0.36.22)
> 26 November 2020
- Add XML escaping to XMP sidecar export, thanks to @jstrine for the fix! [`#272`](https://github.com/RhetTbull/osxphotos/pull/272)
- Fix EXIF GPS format for XMP sidecar, thanks to @jstrine for the fix! [`#270`](https://github.com/RhetTbull/osxphotos/pull/270)
- Continue even if the original filename is None, thanks to @jstrine for the fix! [`#268`](https://github.com/RhetTbull/osxphotos/pull/268)
- Added test for missing original_filename [`116cb66`](https://github.com/RhetTbull/osxphotos/commit/116cb662fbddf9153f6858c6ea97dc7f65c77705)
- Add @jstrine as a contributor [`7460bc8`](https://github.com/RhetTbull/osxphotos/commit/7460bc88fcc5e1e7435c9b9bcdf7ec9c7c5e39ea)
- Escape characters which cause XML parsing issues [`c42050a`](https://github.com/RhetTbull/osxphotos/commit/c42050a10cac40b0b5ac70c587e07f257a9b50dd)
- Fix tests for apostrophe [`d0d2e80`](https://github.com/RhetTbull/osxphotos/commit/d0d2e8080096bf66f93a830386800ce713680c51)
- Fix test for XMP sidecar with GPS info [`c27cfb1`](https://github.com/RhetTbull/osxphotos/commit/c27cfb1223fa82b9e5549b93c283e9444693270a)
#### [v0.36.21](https://github.com/RhetTbull/osxphotos/compare/v0.36.20...v0.36.21)
> 25 November 2020
- Exposed --use-photos-export and --use-photokit [`e951e53`](https://github.com/RhetTbull/osxphotos/commit/e951e5361e59060229787bb1ea3fc4e088ffff99)
#### [v0.36.20](https://github.com/RhetTbull/osxphotos/compare/v0.36.19...v0.36.20)
> 23 November 2020
- Added photokit export as hidden --use-photokit option [`26f96d5`](https://github.com/RhetTbull/osxphotos/commit/26f96d582c01ce9816b1f54f0e74c8570f133f7c)
#### [v0.36.19](https://github.com/RhetTbull/osxphotos/compare/v0.36.18...v0.36.19)
> 19 November 2020
- Removed debug statement in _photoinfo_export [`8cb15d1`](https://github.com/RhetTbull/osxphotos/commit/8cb15d15551094dcaf1b0ef32d6ac0273be7fd37)
#### [v0.36.18](https://github.com/RhetTbull/osxphotos/compare/v0.36.17...v0.36.18)
> 14 November 2020
- Moved AppleScript to photoscript [`3c85f26`](https://github.com/RhetTbull/osxphotos/commit/3c85f26f901645ce297685ccd639792757fbc995)
- Fixed missing data file for photoscript [`2d9429c`](https://github.com/RhetTbull/osxphotos/commit/2d9429c8eefabe6233fc580f65511c48ee6c01e5)
- Version bump, updated requirements [`3b6dd08`](https://github.com/RhetTbull/osxphotos/commit/3b6dd08d2bb2b20a55064bf24fe7ce788e7268ef)
#### [v0.36.17](https://github.com/RhetTbull/osxphotos/compare/v0.36.15...v0.36.17)
> 12 November 2020
- Fixed path for photos actually missing off disk [`5d4d7d7`](https://github.com/RhetTbull/osxphotos/commit/5d4d7d7db7ca1109b6230803fe777d7a30882efe)
- Fixed erroneous attempt to export edited with --download-missing [`8dc59cb`](https://github.com/RhetTbull/osxphotos/commit/8dc59cbc35c33e71d0d912f4139e855180ac4fbd)
- version bump [`802e2f0`](https://github.com/RhetTbull/osxphotos/commit/802e2f069a5f8b37ddc6b3b8ba07519ce10f88a7)
#### [v0.36.15](https://github.com/RhetTbull/osxphotos/compare/v0.36.14...v0.36.15)
> 11 November 2020
- Avoid copying db files if not necessary [`ea9b41b`](https://github.com/RhetTbull/osxphotos/commit/ea9b41bae41a05aad53454f67871c5e6c9a49f79)
#### [v0.36.14](https://github.com/RhetTbull/osxphotos/compare/v0.36.13...v0.36.14)
> 9 November 2020
- Fix for issue #247 [`38397b5`](https://github.com/RhetTbull/osxphotos/commit/38397b507b456169cf3be2d2dc6743ec8653feb3)
#### [v0.36.13](https://github.com/RhetTbull/osxphotos/compare/v0.36.11...v0.36.13)
> 9 November 2020
- Refactored phototemplate.py to add PATH_SEP option [`3636fcb`](https://github.com/RhetTbull/osxphotos/commit/3636fcbc76100d9898a59f24ed6e9b1965cc6022)
- More work on phototemplate.py to add inline expansion [`a6231e2`](https://github.com/RhetTbull/osxphotos/commit/a6231e29ff28b2c7dc3239445f41afcb35926a7a)
#### [v0.36.11](https://github.com/RhetTbull/osxphotos/compare/v0.36.10...v0.36.11)
> 8 November 2020
- Implemented boolean type template fields [`7fa3704`](https://github.com/RhetTbull/osxphotos/commit/7fa3704840f7800689b4ac5f8edee8210eb3e8db)
- Bug fix in handling missing edited photos [`e829212`](https://github.com/RhetTbull/osxphotos/commit/e829212987bbc1a88f845922abcffef70c159883)
- Fixed message in CLI [`df37a01`](https://github.com/RhetTbull/osxphotos/commit/df37a017a8efdc8d0b9bc8d00a4452dc4cb892b3)
#### [v0.36.10](https://github.com/RhetTbull/osxphotos/compare/v0.36.9...v0.36.10)
> 8 November 2020
- Implemented issue #255 [`ae2fd2e`](https://github.com/RhetTbull/osxphotos/commit/ae2fd2e3db984756e6cc3f7b3338b8ba819ce28c)
#### [v0.36.9](https://github.com/RhetTbull/osxphotos/compare/v0.36.8...v0.36.9)
> 7 November 2020
- Refactored regex in phototemplate [`653b7e6`](https://github.com/RhetTbull/osxphotos/commit/653b7e6600e0738ecd00f74d510a893e0d447ca4)
- Fix for exporting slow mo videos, issue #252 [`9d38885`](https://github.com/RhetTbull/osxphotos/commit/9d38885416b528bd8c91bb09120be85a8b109f29)
#### [v0.36.8](https://github.com/RhetTbull/osxphotos/compare/v0.36.7...v0.36.8)
> 5 November 2020
- Refactored exiftool.py [`2202f1b`](https://github.com/RhetTbull/osxphotos/commit/2202f1b1e9c4f83558ef48e58cb94af6b3a38cdd)
- README.md update [`a509ef1`](https://github.com/RhetTbull/osxphotos/commit/a509ef18d3db2ac15a661e763a7254974cf8d84a)
#### [v0.36.7](https://github.com/RhetTbull/osxphotos/compare/v0.36.6...v0.36.7)
> 4 November 2020
- Implemented context manager for ExifTool, closes #250 [`#250`](https://github.com/RhetTbull/osxphotos/issues/250)
#### [v0.36.6](https://github.com/RhetTbull/osxphotos/compare/v0.36.5...v0.36.6)
> 2 November 2020
- Fix for issue #39 [`c7c5320`](https://github.com/RhetTbull/osxphotos/commit/c7c5320587e31070b55cc8c7e74f30b0f9e61379)
#### [v0.36.5](https://github.com/RhetTbull/osxphotos/compare/v0.36.4...v0.36.5)
> 1 November 2020
- Added --ignore-date-modified flag, issue #247 [`663e33b`](https://github.com/RhetTbull/osxphotos/commit/663e33bc1709f767e1a08242f6bfe86a3fc78552)
#### [v0.36.4](https://github.com/RhetTbull/osxphotos/compare/v0.36.2...v0.36.4)
> 1 November 2020
- Updated --exiftool to set dates/times as Photos does, issue #247 [`11459d1`](https://github.com/RhetTbull/osxphotos/commit/11459d1da4d7d13e36e9db4bdc940b74baad9d11)
- Partial fix for issue #247 on Mojave [`6ac3111`](https://github.com/RhetTbull/osxphotos/commit/6ac311199e9f7afe6170cbbd68ceaa1bb9f0682b)
- Add @mwort as a contributor [`9cff8e8`](https://github.com/RhetTbull/osxphotos/commit/9cff8e89c6e939d3d371a4f60649f6e5595a55b9)
#### [v0.36.2](https://github.com/RhetTbull/osxphotos/compare/v0.36.1...v0.36.2)
> 31 October 2020
- Fixed handling of date_modified for Catalina, issue #247 [`0cce234`](https://github.com/RhetTbull/osxphotos/commit/0cce234a8cbba63dc1cba439c06fe9de078ff480)
#### [v0.36.1](https://github.com/RhetTbull/osxphotos/compare/v0.36.0...v0.36.1)
> 30 October 2020
- Added --has-comment/--has-likes to CLI, issue #240 [`c5dba8c`](https://github.com/RhetTbull/osxphotos/commit/c5dba8c89bba35d7a77e087b180b2a3d7b94280a)
- Cleaned up as_dict/asdict, issue #144, #188 [`603dabb`](https://github.com/RhetTbull/osxphotos/commit/603dabb8f420a89e993d5aadcd3a5614bbb262dd)
- Updated README.md [`d16932d`](https://github.com/RhetTbull/osxphotos/commit/d16932d0fd8d160ccf44e9842329d5933dc25b36)
#### [v0.36.0](https://github.com/RhetTbull/osxphotos/compare/v0.35.7...v0.36.0)
> 26 October 2020
- Added verbose to PhotosDB(), partial fix for #110 [`d87b8f3`](https://github.com/RhetTbull/osxphotos/commit/d87b8f30a45cbb6fdb315a12f8585e2bdc21be6b)
- Added comments/likes, implements #214 [`23de6b5`](https://github.com/RhetTbull/osxphotos/commit/23de6b58908371d9ca55d1d1999c6d56de454180)
- Cleaned up constructor for PhotosDB [`667c89e`](https://github.com/RhetTbull/osxphotos/commit/667c89e32c3f96baeafebc03e83517ea05693b00)
#### [v0.35.7](https://github.com/RhetTbull/osxphotos/compare/v0.35.6...v0.35.7)
> 24 October 2020
- Fix for issue #238 [`48f29e1`](https://github.com/RhetTbull/osxphotos/commit/48f29e138e4e9da3eba78f3681ee9b8cb28910df)
#### [v0.35.6](https://github.com/RhetTbull/osxphotos/compare/v0.35.5...v0.35.6)
> 24 October 2020
- Fixed shared, not_shared in cli [`8551981`](https://github.com/RhetTbull/osxphotos/commit/8551981f68f0cd2a3a081cc21ae287ff981b9b4b)
#### [v0.35.5](https://github.com/RhetTbull/osxphotos/compare/v0.35.4...v0.35.5)
> 22 October 2020
- Added get_shared_photo_comments.py to examples [`15e0914`](https://github.com/RhetTbull/osxphotos/commit/15e0914af6301a945bc751173aef6718487d9637)
- Fix for issue #237 [`a416de2`](https://github.com/RhetTbull/osxphotos/commit/a416de29e4ac39a5c323f7913b05a8c38ad205be)
- Added test for issue #235 [`ea68229`](https://github.com/RhetTbull/osxphotos/commit/ea68229ddac2e2301ac2d5607451cf7d00207d5d)
#### [v0.35.4](https://github.com/RhetTbull/osxphotos/compare/v0.35.3...v0.35.4)
> 18 October 2020
- refactored template code to fix #213 [`#213`](https://github.com/RhetTbull/osxphotos/issues/213)
#### [v0.35.3](https://github.com/RhetTbull/osxphotos/compare/v0.35.2...v0.35.3)
> 15 October 2020
- Fix for issue #235, #236 [`41b2399`](https://github.com/RhetTbull/osxphotos/commit/41b23991df3d1d553b70889ede237f83b6874519)
#### [v0.35.2](https://github.com/RhetTbull/osxphotos/compare/v0.35.1...v0.35.2)
> 12 October 2020
- Fix for issue #234 [`da100f9`](https://github.com/RhetTbull/osxphotos/commit/da100f93a9b849ca4750336d7f90e9023e39dd07)
#### [v0.35.1](https://github.com/RhetTbull/osxphotos/compare/v0.35.0...v0.35.1)
> 12 October 2020
- Fix for issue #230 [`dcbf8f2`](https://github.com/RhetTbull/osxphotos/commit/dcbf8f25f61e21bcf1040046aa9d6ddba4ac9735)
#### [v0.35.0](https://github.com/RhetTbull/osxphotos/compare/v0.34.5...v0.35.0)
> 12 October 2020
- Convert to jpeg [`#233`](https://github.com/RhetTbull/osxphotos/pull/233)
- Updated tests, closes #231 [`#231`](https://github.com/RhetTbull/osxphotos/issues/231)
- Updated tests [`b0171ba`](https://github.com/RhetTbull/osxphotos/commit/b0171ba6f5b73e1ff71e16d27852f8df7f208f60)
- Updated tests [`07b0843`](https://github.com/RhetTbull/osxphotos/commit/07b08433df5a60f191e23a95394e83e51dca016f)
- Merge branch 'master' into convert_to_jpeg [`fe5185b`](https://github.com/RhetTbull/osxphotos/commit/fe5185be8893002da663039f8ec103faed0f1831)
- Added israw, tests for Big Sur [`b5a9794`](https://github.com/RhetTbull/osxphotos/commit/b5a9794f6bff5683fd42a22197454940e4d7ba88)
- Updates to path, path_raw, uti for RAW+JPEG pairs [`b32f4b8`](https://github.com/RhetTbull/osxphotos/commit/b32f4b8504768a5f4b5ad54c00315b9e82fca980)
#### [v0.34.5](https://github.com/RhetTbull/osxphotos/compare/v0.34.3...v0.34.5)
> 6 October 2020
- --convert-to-jpeg initial version working [`38f201d`](https://github.com/RhetTbull/osxphotos/commit/38f201d0fb70bf299a828c1dd0d034a119e380c4)
- Added tests, fixed bug in export_db [`5a13605`](https://github.com/RhetTbull/osxphotos/commit/5a13605f850bb947c8888246f06a5ca4e6aa5f10)
- Updated tests [`b2b39aa`](https://github.com/RhetTbull/osxphotos/commit/b2b39aa6075df11861cf5d8945b657204f120e87)
#### [v0.34.3](https://github.com/RhetTbull/osxphotos/compare/v0.34.2...v0.34.3)
> 29 September 2020
- Update exiftool.py to preserve file modification time, thanks to @hhoeck [`#223`](https://github.com/RhetTbull/osxphotos/pull/223)
- Added tests for 10.15.6 [`432da7f`](https://github.com/RhetTbull/osxphotos/commit/432da7f139a5e4b37eeb358f4ede45314407f8e5)
- Added HEIC test image [`ddc1e69`](https://github.com/RhetTbull/osxphotos/commit/ddc1e69b4a4ac712e1af312b865c4216f9ad350c)
- Fixed bug related to issue #222 [`c939df7`](https://github.com/RhetTbull/osxphotos/commit/c939df717159e8b97955c0b267327cd56a9ed56c)
- Version bump for bug fix [`62d54cc`](https://github.com/RhetTbull/osxphotos/commit/62d54cc0beabd0141545608184d4b2c658eedf0f)
- Update README.md [`6883fec`](https://github.com/RhetTbull/osxphotos/commit/6883fec2b2236d892b88327e1b4e9da1237f7dea)
#### [v0.34.2](https://github.com/RhetTbull/osxphotos/compare/v0.34.1...v0.34.2)
> 14 September 2020
- Partial fix for issue #213 [`459d91d`](https://github.com/RhetTbull/osxphotos/commit/459d91d7b11dbd4b0564906c1689b60dc5b64642)
#### [v0.34.1](https://github.com/RhetTbull/osxphotos/compare/v0.34.0...v0.34.1)
> 13 September 2020
- Fixed exception handling in export [`eb00ffd`](https://github.com/RhetTbull/osxphotos/commit/eb00ffd73737ef4832229e4e6fd8dc4ccb0b8539)
- Updated README.md [`a1776fa`](https://github.com/RhetTbull/osxphotos/commit/a1776fa14850275ad6b02ece80bbe8ce908fa836)
#### [v0.34.0](https://github.com/RhetTbull/osxphotos/compare/v0.33.8...v0.34.0)
> 7 September 2020
- Added --skip-original-if-edited for issue #159 [`5f2d401`](https://github.com/RhetTbull/osxphotos/commit/5f2d401048850fd68f31b37a7e71abc11ca80dc5)
- Still working on issue #208 [`58b3869`](https://github.com/RhetTbull/osxphotos/commit/58b3869a7cce7cb3f211599e544d7e5426ceb4a6)
#### [v0.33.8](https://github.com/RhetTbull/osxphotos/compare/v0.33.7...v0.33.8)
> 31 August 2020
- Fixed sidecar collisions, closes #210 [`#210`](https://github.com/RhetTbull/osxphotos/issues/210)
#### [v0.33.7](https://github.com/RhetTbull/osxphotos/compare/v0.33.5...v0.33.7)
> 31 August 2020
- typo fix - thanks to @dmd [`#212`](https://github.com/RhetTbull/osxphotos/pull/212)
- Normalize unicode for issue #208 [`a36eb41`](https://github.com/RhetTbull/osxphotos/commit/a36eb416b19284477922b6a5f837f4040327138b)
- Added force_download.py to examples [`b611d34`](https://github.com/RhetTbull/osxphotos/commit/b611d34d19db480af72f57ef55eacd0a32c8d1e8)
- Added photoshop:SidecarForExtension to XMP, partial fix for #210 [`60d96a8`](https://github.com/RhetTbull/osxphotos/commit/60d96a8f563882fba2365a6ab58c1276725eedaa)
- Updated README.md [`c9b1518`](https://github.com/RhetTbull/osxphotos/commit/c9b15186a022d91248451279e5f973e3f2dca4b4)
- Update README.md [`42e8fba`](https://github.com/RhetTbull/osxphotos/commit/42e8fba125a3c6b1bd0d538f2af511aabfbeb478)
#### [v0.33.5](https://github.com/RhetTbull/osxphotos/compare/v0.33.3...v0.33.5)
> 25 August 2020
- Fixed DST handling for from_date/to_date, closes #193 (again) [`#193`](https://github.com/RhetTbull/osxphotos/issues/193)
- Added raw timestamps to PhotoInfo._info [`0f457a4`](https://github.com/RhetTbull/osxphotos/commit/0f457a4082a4eebc42a5df2160a02ad987b6f96c)
#### [v0.33.3](https://github.com/RhetTbull/osxphotos/compare/v0.33.2...v0.33.3)
> 23 August 2020
- Fixed portrait for Catalina/Big Sur; see issue #203 [`1f717b0`](https://github.com/RhetTbull/osxphotos/commit/1f717b05794c2088c7c15d2aab0c5d24b6309c06)
#### [v0.33.2](https://github.com/RhetTbull/osxphotos/compare/v0.33.0...v0.33.2)
> 23 August 2020
- Closes issue #206, adds --touch-file [`#207`](https://github.com/RhetTbull/osxphotos/pull/207)
- Touch files - fixes #194 -- thanks to @PabloKohan [`#205`](https://github.com/RhetTbull/osxphotos/pull/205)
- Refactor/cleanup _export_photo - thanks to @PabloKohan [`#204`](https://github.com/RhetTbull/osxphotos/pull/204)
- Finished --touch-file, closes #206 [`#206`](https://github.com/RhetTbull/osxphotos/issues/206)
- Merge pull request #205 from PabloKohan/touch_files__fix_194 [`#194`](https://github.com/RhetTbull/osxphotos/issues/194)
- --touch-file now working with --update [`6c11e3f`](https://github.com/RhetTbull/osxphotos/commit/6c11e3fa5b5b05b98b9fdbb0e59e3a78c7dff980)
- Refactor/cleanup _export_photo [`eefa1f1`](https://github.com/RhetTbull/osxphotos/commit/eefa1f181f4fd7b027ae69abd2b764afb590c081)
- Fixed touch tests [`1bf7105`](https://github.com/RhetTbull/osxphotos/commit/1bf7105737fbd756064a2f9ef4d4bbd0b067978c)
- Working on issue 206 [`ebd878a`](https://github.com/RhetTbull/osxphotos/commit/ebd878a075983ef3df0b1ead1a725e01508721f8)
- Working on issue #206 [`c9c9202`](https://github.com/RhetTbull/osxphotos/commit/c9c920220545dc27c8cb1379d7bde15987cce72c)
#### [v0.33.0](https://github.com/RhetTbull/osxphotos/compare/v0.32.0...v0.33.0)
> 17 August 2020
- Replaced call to which, closes #171 [`#171`](https://github.com/RhetTbull/osxphotos/issues/171)
- Added contributors to README.md, closes #200 [`#200`](https://github.com/RhetTbull/osxphotos/issues/200)
- Added tests for 10.15.6 [`d2deeff`](https://github.com/RhetTbull/osxphotos/commit/d2deefff834e46e1a26adc01b1b025ac839dbc78)
- Added ImportInfo for Photos 5+ [`98e4170`](https://github.com/RhetTbull/osxphotos/commit/98e417023ec5bd8292b25040d0844f3706645950)
- Update README.md [`360c8d8`](https://github.com/RhetTbull/osxphotos/commit/360c8d8e1b4760e95a8b71b3a0bf0df4fb5adaf5)
- Update README.md [`868cda8`](https://github.com/RhetTbull/osxphotos/commit/868cda8482ce6b29dd00e04a209d40550e6b128b)
#### [v0.32.0](https://github.com/RhetTbull/osxphotos/compare/v0.31.2...v0.32.0)
> 9 August 2020
- Alpha support for MacOS Big Sur/10.16, see issue #187 [`6acf9ac`](https://github.com/RhetTbull/osxphotos/commit/6acf9acd6364e1996158179493d128ec0958e652)
#### [v0.31.2](https://github.com/RhetTbull/osxphotos/compare/v0.31.0...v0.31.2)
> 9 August 2020
- Fixed from_date and to_date to be timezone aware, closes #193 [`#193`](https://github.com/RhetTbull/osxphotos/issues/193)
- Added test for valid XMP file, closes #197 [`#197`](https://github.com/RhetTbull/osxphotos/issues/197)
- Dropped py36 due to datetime.fromisoformat [`a714ae0`](https://github.com/RhetTbull/osxphotos/commit/a714ae0af089b13acf70c4f29934393aa48ed222)
- Added --uuid-from-file to CLI [`840e993`](https://github.com/RhetTbull/osxphotos/commit/840e9937bede407ef55972a361618683245e086b)
- Added write_uuid_to_file.applescript to utils [`bea770b`](https://github.com/RhetTbull/osxphotos/commit/bea770b322d21cf3f8245d20e182006247cb71d6)
- Updated README.md [`002fce8`](https://github.com/RhetTbull/osxphotos/commit/002fce8e93edd936d4b866118ae6d4c94e5d6744)
- Added py37 [`d0ec862`](https://github.com/RhetTbull/osxphotos/commit/d0ec8620c721fe7576ab7d519a5eaac4d17a317e)
#### [v0.31.0](https://github.com/RhetTbull/osxphotos/compare/v0.30.13...v0.31.0)
> 27 July 2020
- Initial FaceInfo support for Issue #21 [`6f29cda`](https://github.com/RhetTbull/osxphotos/commit/6f29cda99f1b8d94a95597c7046620cf21fecae4)
- Updated Github Actions to run on PR [`9fc4f76`](https://github.com/RhetTbull/osxphotos/commit/9fc4f762193699dd45b586b51aa2d3066928aab1)
#### [v0.30.13](https://github.com/RhetTbull/osxphotos/compare/v0.30.12...v0.30.13)
> 23 July 2020
- This reverts commit b7f4b739de978991def8ae2dca0f4e4b2881f56d, reversing [`#191`](https://github.com/RhetTbull/osxphotos/pull/191)
- Fix findfiles not to fail on missing/invalid dir [`#192`](https://github.com/RhetTbull/osxphotos/pull/192)
- Revert "Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133)" [`#191`](https://github.com/RhetTbull/osxphotos/pull/191)
- Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133) [`#190`](https://github.com/RhetTbull/osxphotos/pull/190)
- Fix findfiles not to fail on missing/invalid dir [`8c10b61`](https://github.com/RhetTbull/osxphotos/commit/8c10b61e90abbcfdff472bad4bb760558c7b850c)
- Revert "Fix FileExistsError when filename differs only in case and export-as-hardlink (Bug#133)" [`f8e62d8`](https://github.com/RhetTbull/osxphotos/commit/f8e62d8f5ed26814f02383426237fd4c99a7ad04)
- Fix FileExistsError when filename differs only in case and export-as-hardlink [`d52b387`](https://github.com/RhetTbull/osxphotos/commit/d52b387a294e68ebf0580a202ea70b97205560ef)
- Version bump for bug fix [`cf4dca1`](https://github.com/RhetTbull/osxphotos/commit/cf4dca10c02d5f3f6132ab1572a698379b667e48)
#### [v0.30.12](https://github.com/RhetTbull/osxphotos/compare/v0.30.10...v0.30.12)
> 18 July 2020
- Implemented PersonInfo, closes #181 [`#181`](https://github.com/RhetTbull/osxphotos/issues/181)
- Updated dependencies, now supports py36, py37, py38 [`6688d1f`](https://github.com/RhetTbull/osxphotos/commit/6688d1ff6491f2e7e155946b265ef8b5d8929441)
- Update README.md [`3526881`](https://github.com/RhetTbull/osxphotos/commit/3526881ec872cc009b0d8936f366afcfff166d42)
#### [v0.30.10](https://github.com/RhetTbull/osxphotos/compare/v0.30.9...v0.30.10)
> 6 July 2020
- Bug fix for empty albums [`1ef518c`](https://github.com/RhetTbull/osxphotos/commit/1ef518cc3e9efbe9d4c16aa3d36c6dc6db86798e)
#### [v0.30.9](https://github.com/RhetTbull/osxphotos/compare/v0.30.7...v0.30.9)
> 6 July 2020
- Refactored person processing to enable implementation of #181 [`fcff8ec`](https://github.com/RhetTbull/osxphotos/commit/fcff8ec5f8286b28e7d8559b40b5808a7b59cc15)
- AlbumInfo.photos now returns photos in album sort order [`9d820a0`](https://github.com/RhetTbull/osxphotos/commit/9d820a0557944340d0c664a6c3497d138c6100d5)
#### [v0.30.7](https://github.com/RhetTbull/osxphotos/compare/v0.30.6...v0.30.7)
> 4 July 2020
- Bug fix for keywords, persons in deleted photos [`df75a05`](https://github.com/RhetTbull/osxphotos/commit/df75a05645a88b31daa411f960d99ade71efc908)
#### [v0.30.6](https://github.com/RhetTbull/osxphotos/compare/v0.30.5...v0.30.6)
> 3 July 2020
- Added height, width, orientation, filesize to json, str) [`8c3af0a`](https://github.com/RhetTbull/osxphotos/commit/8c3af0a4e4e49d9bbb33e809973d958334e44dca)
#### [v0.30.5](https://github.com/RhetTbull/osxphotos/compare/v0.30.4...v0.30.5)
> 3 July 2020
- Added height, width, orientation, filesize, closes #163 [`#163`](https://github.com/RhetTbull/osxphotos/issues/163)
#### [v0.30.4](https://github.com/RhetTbull/osxphotos/compare/v0.30.3...v0.30.4)
> 3 July 2020
- Added GPS location to XMP sidecar, closes #175 [`#175`](https://github.com/RhetTbull/osxphotos/issues/175)
- Updated README.md [`7806e05`](https://github.com/RhetTbull/osxphotos/commit/7806e05673775ded231e65f53f3a1d5095a4b4e1)
#### [v0.30.3](https://github.com/RhetTbull/osxphotos/compare/v0.30.2...v0.30.3)
> 29 June 2020
- Added --description-template to CLI, closes #166 [`#166`](https://github.com/RhetTbull/osxphotos/issues/166)
- Added expand_inplace to PhotoTemplate.render [`ff03287`](https://github.com/RhetTbull/osxphotos/commit/ff0328785f3ea14b1c8ae2b7d1a9b07e8aef0777)
- Updated README.md [`5950707`](https://github.com/RhetTbull/osxphotos/commit/59507077bafe39a17bc23babe6d6c52e1f502a53)
#### [v0.30.2](https://github.com/RhetTbull/osxphotos/compare/v0.30.1...v0.30.2)
> 28 June 2020
- Added --deleted, --deleted-only to CLI, closes #179 [`#179`](https://github.com/RhetTbull/osxphotos/issues/179)
#### [v0.30.1](https://github.com/RhetTbull/osxphotos/compare/v0.30.0...v0.30.1)
> 27 June 2020

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Rhet Turnbull
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

View File

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

1818
README.md

File diff suppressed because it is too large Load Diff

271
README.rst Normal file
View File

@@ -0,0 +1,271 @@
.. role:: raw-html-m2r(raw)
:format: html
OSXPhotos
=========
What is osxphotos?
------------------
OSXPhotos provides both the ability to interact with and query Apple's Photos.app library on macOS directly from your python code
as well as a very flexible command line interface (CLI) app for exporting photos.
You can query the Photos library database -- for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
You can also easily export both the original and edited photos.
Supported operating systems
---------------------------
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
Beta support for macOS Big Sur (10.16.01/11.01).
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``.
Installation
------------
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.
Installation using pipx
^^^^^^^^^^^^^^^^^^^^^^^
If you aren't familiar with installing python applications, I recommend you install ``osxphotos`` with `pipx <https://github.com/pipxproject/pipx>`_. If you use ``pipx``\ , you will not need to create a virtual environment as ``pipx`` takes care of this. The easiest way to do this on a Mac is to use `homebrew <https://brew.sh/>`_\ :
* Open ``Terminal`` (search for ``Terminal`` in Spotlight or look in ``Applications/Utilities``\ )
* Install ``homebrew`` according to instructions at `https://brew.sh/ <https://brew.sh/>`_
* Type the following into Terminal: ``brew install pipx``
* Then type this: ``pipx install osxphotos``
* Now you should be able to run ``osxphotos`` by typing: ``osxphotos``
Installation using pip
^^^^^^^^^^^^^^^^^^^^^^
You can also install directly from `pypi <https://pypi.org/project/osxphotos/>`_\ :
.. code-block::
pip install osxphotos
Installation from git repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSXPhotos uses setuptools, thus simply run:
.. code-block::
git clone https://github.com/RhetTbull/osxphotos.git
cd osxphotos
python3 setup.py install
I recommend you create a `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ before installing osxphotos.
**WARNING** The git repo for this project is very large (> 1GB) because it contains multiple Photos libraries used for testing
on different versions of macOS. If you just want to use the osxphotos package in your own code,
I recommend you install the latest version from `PyPI <https://pypi.org/project/osxphotos/>`_ which does not include all the test
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
`release <https://github.com/RhetTbull/osxphotos/releases>`_ or you can install via ``pip`` which also installs the command line app.
If you aren't comfortable with running python on your Mac, start with the pre-built executable or ``pipx`` as described above.
Command Line Usage
------------------
This package will install a command line utility called ``osxphotos`` that allows you to query the Photos database and export photos.
Alternatively, you can also run the command line utility like this: ``python3 -m osxphotos``
.. code-block::
> osxphotos
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
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.
-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...
To get help on a specific command, use ``osxphotos help <command_name>``
Command line examples
^^^^^^^^^^^^^^^^^^^^^
export all photos to ~/Desktop/export group in folders by date created
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos export --export-by-date ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export``
**Note**\ : Photos library/database path can also be specified using ``--db`` option:
``osxphotos export --export-by-date --db ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export``
find all photos with keyword "Kids" and output results to json file named results.json:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos query --keyword Kids --json ~/Pictures/Photos\ Library.photoslibrary >results.json``
export photos to file structure based on 4-digit year and full name of month of photo's creation date:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos export ~/Desktop/export --directory "{created.year}/{created.month}"``
(by default, it will attempt to use the system library)
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):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos export ~/Desktop/export --directory "{created.year}" --keyword-template "{label}" --keyword-template "{media_type}"``
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose``
Example uses of the package
---------------------------
.. code-block:: python
""" Simple usage of the package """
import osxphotos
def main():
photosdb = osxphotos.PhotosDB()
print(photosdb.keywords)
print(photosdb.persons)
print(photosdb.album_names)
print(photosdb.keywords_as_dict)
print(photosdb.persons_as_dict)
print(photosdb.albums_as_dict)
# find all photos with Keyword = Foo and containing John Smith
photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"])
# find all photos that include Alice Smith but do not contain the keyword Bar
photos = [p for p in photosdb.photos(persons=["Alice Smith"])
if p not in photosdb.photos(keywords=["Bar"]) ]
for p in photos:
print(
p.uuid,
p.filename,
p.original_filename,
p.date,
p.description,
p.title,
p.keywords,
p.albums,
p.persons,
p.path,
)
if __name__ == "__main__":
main()
.. code-block:: python
""" Export all photos to specified directory using album names as folders
If file has been edited, also export the edited version,
otherwise, export the original version
This will result in duplicate photos if photo is in more than album """
import os.path
import pathlib
import sys
import click
from pathvalidate import is_valid_filepath, sanitize_filepath
import osxphotos
@click.command()
@click.argument("export_path", type=click.Path(exists=True))
@click.option(
"--default-album",
help="Default folder for photos with no album. Defaults to 'unfiled'",
default="unfiled",
)
@click.option(
"--library-path",
help="Path to Photos library, default to last used library",
default=None,
)
def export(export_path, default_album, library_path):
export_path = os.path.expanduser(export_path)
library_path = os.path.expanduser(library_path) if library_path else None
if library_path is not None:
photosdb = osxphotos.PhotosDB(library_path)
else:
photosdb = osxphotos.PhotosDB()
photos = photosdb.photos()
for p in photos:
if not p.ismissing:
albums = p.albums
if not albums:
albums = [default_album]
for album in albums:
click.echo(f"exporting {p.filename} in album {album}")
# make sure no invalid characters in destination path (could be in album name)
album_name = sanitize_filepath(album, platform="auto")
# create destination folder, if necessary, based on album name
dest_dir = os.path.join(export_path, album_name)
# verify path is a valid path
if not is_valid_filepath(dest_dir, platform="auto"):
sys.exit(f"Invalid filepath {dest_dir}")
# create destination dir if needed
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
# export the photo
if p.hasadjustments:
# export edited version
exported = p.export(dest_dir, edited=True)
edited_name = pathlib.Path(p.path_edited).name
click.echo(f"Exported {edited_name} to {exported}")
# export unedited version
exported = p.export(dest_dir)
click.echo(f"Exported {p.filename} to {exported}")
else:
click.echo(f"Skipping missing photo: {p.filename}")
if __name__ == "__main__":
export() # pylint: disable=no-value-for-parameter
Package Interface
-----------------
Reference full documentation on `GitHub <https://github.com/RhetTbull/osxphotos/blob/master/README.md>`_

5
cli.py
View File

@@ -3,8 +3,7 @@
To build this into an executable:
- install pyinstaller:
python3 -m pip install pyinstaller
- then use make_cli_exe.sh to run pyinstaller or execute the following command:
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
- then use make_cli_exe.sh to run pyinstaller
Resulting executable will be in "dist/osxphotos"
@@ -13,7 +12,7 @@
"""
from osxphotos.__main__ import cli
from osxphotos.cli import cli
if __name__ == "__main__":
cli()

4
docs/.buildinfo Normal file
View File

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

0
docs/.nojekyll Normal file
View File

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

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.41.0 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>All modules for which code is available</h1>
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
</ul>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="../reference.html">osxphotos package</a></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<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.4.3</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.41.0 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.4.3</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.0 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.4.3</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.0 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.4.3</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

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

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

@@ -0,0 +1,856 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.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, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.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 {
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,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.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 {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption {
padding: 0.3em;
}
div.figure p.caption span.caption-number {
font-style: italic;
}
div.figure p.caption 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;
}
/* -- 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;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.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.5em;
content: ":";
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
}
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.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
code.descclassname {
background-color: transparent;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

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

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

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

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

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

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

BIN
docs/_static/file.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

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

File diff suppressed because it is too large Load Diff

2
docs/_static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

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

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

BIN
docs/_static/minus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
docs/_static/plus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

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

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

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

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

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

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

31
docs/_static/underscore.js vendored Normal file
View File

@@ -0,0 +1,31 @@
// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,
h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each=
b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===n)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===n)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(x&&a.map===x)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==
null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=
function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e=
e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&(e={value:a,computed:b})});
return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){f==0?b[0]=a:(d=Math.floor(Math.random()*(f+1)),b[f]=b[d],b[d]=a)});return b};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,g){return{value:a,criteria:c.call(d,a,b,g)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,
c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:a.toArray?a.toArray():b.isArray(a)?i.call(a):b.isArguments(a)?i.call(a):b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=
b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,e=[];b.reduce(d,function(d,g,h){if(0==h||(c===true?b.last(d)!=g:!b.include(d,g)))d[d.length]=g,e[e.length]=a[h];return d},[]);
return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,
d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(p&&a.indexOf===p)return a.indexOf(c);for(d=0,e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(D&&a.lastIndexOf===D)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};
var F=function(){};b.bind=function(a,c){var d,e;if(a.bind===s&&s)return s.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));F.prototype=a.prototype;var b=new F,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,
c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i=b.debounce(function(){h=g=false},c);return function(){d=this;e=arguments;var b;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);i()},c));g?h=true:
a.apply(d,e);i();g=true}};b.debounce=function(a,b){var d;return function(){var e=this,f=arguments;clearTimeout(d);d=setTimeout(function(){d=null;a.apply(e,f)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};
b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments,
1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};
b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};
b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.mixin=function(a){j(b.functions(a),
function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+
u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]=
function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=
true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);

1441
docs/cli.html Normal file

File diff suppressed because it is too large Load Diff

2120
docs/genindex.html Normal file

File diff suppressed because it is too large Load Diff

380
docs/index.html Normal file
View File

@@ -0,0 +1,380 @@
<!DOCTYPE html>
<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.41.0 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="next" title="osxphotos command line interface (CLI)" href="cli.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<div class="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">
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="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">
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline"></a></h2>
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
Beta support for macOS Big Sur (10.16.01/11.01).</p>
<p>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">
<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">
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline"></a></h3>
<p>If you arent familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
<ul class="simple">
<li><p>Open <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> (search for <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> in Spotlight or look in <code class="docutils literal notranslate"><span class="pre">Applications/Utilities</span></code>)</p></li>
<li><p>Install <code class="docutils literal notranslate"><span class="pre">homebrew</span></code> according to instructions at <a class="reference external" href="https://brew.sh/">https://brew.sh/</a></p></li>
<li><p>Type the following into Terminal: <code class="docutils literal notranslate"><span class="pre">brew</span> <span class="pre">install</span> <span class="pre">pipx</span></code></p></li>
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
</ul>
</div>
<div class="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">
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline"></a></h3>
<p>OSXPhotos uses setuptools, thus simply run:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
<span class="n">cd</span> <span class="n">osxphotos</span>
<span class="n">python3</span> <span class="n">setup</span><span class="o">.</span><span class="n">py</span> <span class="n">install</span>
</pre></div>
</div>
<p>I recommend you create a <a class="reference external" href="https://docs.python.org/3/tutorial/venv.html">virtual environment</a> before installing osxphotos.</p>
<p><strong>WARNING</strong> The git repo for this project is very large (&gt; 1GB) because it contains multiple Photos libraries used for testing
on different versions of macOS. If you just want to use the osxphotos package in your own code,
I recommend you install the latest version from <a class="reference external" href="https://pypi.org/project/osxphotos/">PyPI</a> which does not include all the test
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
If you arent comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
</div>
</div>
<div class="section" id="command-line-usage">
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline"></a></h2>
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&gt;</span> <span class="n">osxphotos</span>
<span class="n">Usage</span><span class="p">:</span> <span class="n">osxphotos</span> <span class="p">[</span><span class="n">OPTIONS</span><span class="p">]</span> <span class="n">COMMAND</span> <span class="p">[</span><span class="n">ARGS</span><span class="p">]</span><span class="o">...</span>
<span class="n">Options</span><span class="p">:</span>
<span class="o">--</span><span class="n">db</span> <span class="o">&lt;</span><span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">&gt;</span> <span class="n">Specify</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">.</span> <span class="n">Path</span> <span class="n">to</span> <span class="n">Photos</span>
<span class="n">library</span><span class="o">/</span><span class="n">database</span> <span class="n">can</span> <span class="n">be</span> <span class="n">specified</span> <span class="n">using</span> <span class="n">either</span>
<span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">directly</span> <span class="k">as</span> <span class="n">PHOTOS_LIBRARY</span> <span class="n">positional</span>
<span class="n">argument</span><span class="o">.</span> <span class="n">If</span> <span class="n">neither</span> <span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">PHOTOS_LIBRARY</span>
<span class="n">provided</span><span class="p">,</span> <span class="n">will</span> <span class="n">attempt</span> <span class="n">to</span> <span class="n">find</span> <span class="n">the</span> <span class="n">library</span> <span class="n">to</span>
<span class="n">use</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">following</span> <span class="n">order</span><span class="p">:</span> <span class="mf">1.</span> <span class="n">last</span> <span class="n">opened</span>
<span class="n">library</span><span class="p">,</span> <span class="mf">2.</span> <span class="n">system</span> <span class="n">library</span><span class="p">,</span> <span class="mf">3.</span>
<span class="o">~/</span><span class="n">Pictures</span><span class="o">/</span><span class="n">Photos</span> <span class="n">Library</span><span class="o">.</span><span class="n">photoslibrary</span>
<span class="o">--</span><span class="n">json</span> <span class="n">Print</span> <span class="n">output</span> <span class="ow">in</span> <span class="n">JSON</span> <span class="nb">format</span><span class="o">.</span>
<span class="o">-</span><span class="n">v</span><span class="p">,</span> <span class="o">--</span><span class="n">version</span> <span class="n">Show</span> <span class="n">the</span> <span class="n">version</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
<span class="o">-</span><span class="n">h</span><span class="p">,</span> <span class="o">--</span><span class="n">help</span> <span class="n">Show</span> <span class="n">this</span> <span class="n">message</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
<span class="n">Commands</span><span class="p">:</span>
<span class="n">about</span> <span class="n">Print</span> <span class="n">information</span> <span class="n">about</span> <span class="n">osxphotos</span> <span class="n">including</span> <span class="n">license</span><span class="o">.</span>
<span class="n">albums</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">albums</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">dump</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="nb">all</span> <span class="n">photos</span> <span class="o">&amp;</span> <span class="n">associated</span> <span class="n">info</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span><span class="o">...</span>
<span class="n">export</span> <span class="n">Export</span> <span class="n">photos</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span> <span class="n">database</span><span class="o">.</span>
<span class="n">help</span> <span class="n">Print</span> <span class="n">help</span><span class="p">;</span> <span class="k">for</span> <span class="n">help</span> <span class="n">on</span> <span class="n">commands</span><span class="p">:</span> <span class="n">help</span> <span class="o">&lt;</span><span class="n">command</span><span class="o">&gt;.</span>
<span class="n">info</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">descriptive</span> <span class="n">info</span> <span class="n">of</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span> <span class="n">database</span><span class="o">.</span>
<span class="n">keywords</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">keywords</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">labels</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">image</span> <span class="n">classification</span> <span class="n">labels</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span><span class="o">...</span>
<span class="nb">list</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="n">Photos</span> <span class="n">libraries</span> <span class="n">found</span> <span class="n">on</span> <span class="n">the</span> <span class="n">system</span><span class="o">.</span>
<span class="n">persons</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">persons</span> <span class="p">(</span><span class="n">faces</span><span class="p">)</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">places</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">places</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">query</span> <span class="n">Query</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">using</span> <span class="mi">1</span> <span class="ow">or</span> <span class="n">more</span> <span class="n">search</span> <span class="n">options</span><span class="p">;</span> <span class="k">if</span><span class="o">...</span>
</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">
<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">
<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">
<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">
<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">
<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">
<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>
</div>
<div class="section" id="example-uses-of-the-package">
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline"></a></h2>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Simple usage of the package &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">album_names</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords_as_dict</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons_as_dict</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">albums_as_dict</span><span class="p">)</span>
<span class="c1"># find all photos with Keyword = Foo and containing John Smith</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Foo&quot;</span><span class="p">],</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;John Smith&quot;</span><span class="p">])</span>
<span class="c1"># find all photos that include Alice Smith but do not contain the keyword Bar</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Alice Smith&quot;</span><span class="p">])</span>
<span class="k">if</span> <span class="n">p</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;Bar&quot;</span><span class="p">])</span> <span class="p">]</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span>
<span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">albums</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">persons</span><span class="p">,</span>
<span class="n">p</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&quot;__main__&quot;</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</pre></div>
</div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Export all photos to specified directory using album names as folders</span>
<span class="sd"> If file has been edited, also export the edited version,</span>
<span class="sd"> otherwise, export the original version</span>
<span class="sd"> This will result in duplicate photos if photo is in more than album &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">pathvalidate</span> <span class="kn">import</span> <span class="n">is_valid_filepath</span><span class="p">,</span> <span class="n">sanitize_filepath</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s2">&quot;export_path&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">click</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">exists</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
<span class="s2">&quot;--default-album&quot;</span><span class="p">,</span>
<span class="n">help</span><span class="o">=</span><span class="s2">&quot;Default folder for photos with no album. Defaults to &#39;unfiled&#39;&quot;</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="s2">&quot;unfiled&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
<span class="s2">&quot;--library-path&quot;</span><span class="p">,</span>
<span class="n">help</span><span class="o">=</span><span class="s2">&quot;Path to Photos library, default to last used library&quot;</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">export</span><span class="p">(</span><span class="n">export_path</span><span class="p">,</span> <span class="n">default_album</span><span class="p">,</span> <span class="n">library_path</span><span class="p">):</span>
<span class="n">export_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">expanduser</span><span class="p">(</span><span class="n">export_path</span><span class="p">)</span>
<span class="n">library_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">expanduser</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span> <span class="k">if</span> <span class="n">library_path</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">library_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">()</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
<span class="n">albums</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">albums</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">albums</span><span class="p">:</span>
<span class="n">albums</span> <span class="o">=</span> <span class="p">[</span><span class="n">default_album</span><span class="p">]</span>
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="n">albums</span><span class="p">:</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;exporting </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> in album </span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># make sure no invalid characters in destination path (could be in album name)</span>
<span class="n">album_name</span> <span class="o">=</span> <span class="n">sanitize_filepath</span><span class="p">(</span><span class="n">album</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">&quot;auto&quot;</span><span class="p">)</span>
<span class="c1"># create destination folder, if necessary, based on album name</span>
<span class="n">dest_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">export_path</span><span class="p">,</span> <span class="n">album_name</span><span class="p">)</span>
<span class="c1"># verify path is a valid path</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_valid_filepath</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">&quot;auto&quot;</span><span class="p">):</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Invalid filepath </span><span class="si">{</span><span class="n">dest_dir</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># create destination dir if needed</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isdir</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">):</span>
<span class="n">os</span><span class="o">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
<span class="c1"># export the photo</span>
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="c1"># export edited version</span>
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">edited</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Exported </span><span class="si">{</span><span class="n">edited_name</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="c1"># export unedited version</span>
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Exported </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Skipping missing photo: </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&quot;__main__&quot;</span><span class="p">:</span>
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
</pre></div>
</div>
</div>
<div class="section" id="package-interface">
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline"></a></h2>
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
<div class="toctree-wrapper compound">
<ul>
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-persons">persons</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<div class="section" id="indices-and-tables">
<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>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="#">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="#">Documentation overview</a><ul>
<li>Next: <a href="cli.html" title="next chapter">osxphotos command line interface (CLI)</a></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<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.4.3</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/index.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

106
docs/modules.html Normal file
View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.41.0 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">
<div class="section" id="osxphotos">
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="toctree-wrapper compound">
</div>
</div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="index.html">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/modules.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

BIN
docs/objects.inv Normal file

Binary file not shown.

BIN
docs/osxphotos.pdf Normal file

Binary file not shown.

1356
docs/reference.html Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/screencast/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,296 @@
# how to use this file? see https://github.com/faressoft/terminalizer
# running commands:
# mkdir trip
# osxphotos export --export-by-date --from-date 2021-01-01 trip
# du -h trip
# find trip | head -20
# The configurations that used for the recording, feel free to edit them
config:
# Specify a command to be executed
# like `/bin/bash -l`, `ls`, or any other commands
# the default is bash for Linux
# or powershell.exe for Windows
command: zsh
# Specify the current working directory path
# the default is the current working directory path
cwd: /Users/aravindo/Downloads
# Export additional ENV variables
env:
recording: true
# Explicitly set the number of columns
# or use `auto` to take the current
# number of columns of your shell
cols: 91
# Explicitly set the number of rows
# or use `auto` to take the current
# number of rows of your shell
rows: 20
# Amount of times to repeat GIF
# If value is -1, play once
# If value is 0, loop indefinitely
# If value is a positive number, loop n times
repeat: 0
# Quality
# 1 - 100
quality: 100
# Delay between frames in ms
# If the value is `auto` use the actual recording delays
frameDelay: auto
# Maximum delay between frames in ms
# Ignored if the `frameDelay` isn't set to `auto`
# Set to `auto` to prevent limiting the max idle time
maxIdleTime: 2000
# The surrounding frame box
# The `type` can be null, window, floating, or solid`
# To hide the title use the value null
# Don't forget to add a backgroundColor style with a null as type
frameBox:
type: floating
title: ""
style:
border: 0px black solid
# boxShadow: none
# margin: 0px
# Add a watermark image to the rendered gif
# You need to specify an absolute path for
# the image on your machine or a URL, and you can also
# add your own CSS styles
watermark:
imagePath: null
style:
position: absolute
right: 15px
bottom: 15px
width: 100px
opacity: 0.9
# Cursor style can be one of
# `block`, `underline`, or `bar`
cursorStyle: block
# Font family
# You can use any font that is installed on your machine
# in CSS-like syntax
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"
# The size of the font
fontSize: 12
# The height of lines
lineHeight: 1
# The spacing between letters
letterSpacing: 0
# Theme
theme:
background: "transparent"
foreground: "#afafaf"
cursor: "#c7c7c7"
black: "#232628"
red: "#fc4384"
green: "#b3e33b"
yellow: "#ffa727"
blue: "#75dff2"
magenta: "#ae89fe"
cyan: "#708387"
white: "#d5d5d0"
brightBlack: "#626566"
brightRed: "#ff7fac"
brightGreen: "#c8ed71"
brightYellow: "#ebdf86"
brightBlue: "#75dff2"
brightMagenta: "#ae89fe"
brightCyan: "#b1c6ca"
brightWhite: "#f9f9f4"
# Records, feel free to edit them
records:
- delay: 100
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J \e[K\e[?2004h"
- delay: 100
content: m
- delay: 100
content: "\bmk"
- delay: 100
content: d
- delay: 100
content: i
- delay: 100
content: r
- delay: 100
content: ' '
- delay: 100
content: t
- delay: 100
content: r
- delay: 100
content: i
- delay: 100
content: p
- delay: 100
content: "\e[?2004l\r\r\n"
- delay: 9
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J \e[K\e[?2004h"
- delay: 300
content: o
- delay: 100
content: "\bos"
- delay: 100
content: x
- delay: 100
content: p
- delay: 100
content: h
- delay: 100
content: o
- delay: 100
content: t
- delay: 100
content: o
- delay: 100
content: s
- delay: 100
content: ' '
- delay: 100
content: e
- delay: 100
content: x
- delay: 100
content: p
- delay: 100
content: o
- delay: 100
content: r
- delay: 100
content: t
- delay: 100
content: ' '
- delay: 100
content: '-'
- delay: 100
content: '-'
- delay: 100
content: e
- delay: 100
content: x
- delay: 100
content: p
- delay: 100
content: o
- delay: 100
content: r
- delay: 100
content: t
- delay: 100
content: '-'
- delay: 100
content: b
- delay: 100
content: 'y'
- delay: 100
content: '-'
- delay: 100
content: d
- delay: 100
content: a
- delay: 100
content: t
- delay: 100
content: e
- delay: 100
content: ' '
- delay: 100
content: '-'
- delay: 100
content: '-'
- delay: 100
content: f
- delay: 100
content: r
- delay: 100
content: o
- delay: 100
content: m
- delay: 100
content: '-'
- delay: 100
content: d
- delay: 100
content: a
- delay: 100
content: t
- delay: 100
content: e
- delay: 100
content: ' '
- delay: 100
content: '2'
- delay: 100
content: '0'
- delay: 100
content: '2'
- delay: 100
content: '1'
- delay: 100
content: '-'
- delay: 100
content: '0'
- delay: 100
content: '1'
- delay: 100
content: '-'
- delay: 100
content: '0'
- delay: 100
content: '1'
- delay: 100
content: ' '
- delay: 100
content: t
- delay: 100
content: r
- delay: 100
content: i
- delay: 100
content: p
- delay: 300
content: "\e[?2004l\r\r\n"
- delay: 500
content: "Using last opened Photos library: /Users/user/Pictures/Photos Library.photoslibrary\r\n"
- delay: 8204
content: "Exporting 79 photos to /Users/user/trip...\r\n"
- delay: 321
content: "Processed: 79 photos, exported: 80, missing: 0, error: 0\r\nElapsed time: 0.321 seconds\r\n"
- delay: 317
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J \e[K\e[?2004h"
- delay: 4252
content: "\e[7mdu -h trip\e[27m"
- delay: 487
content: "\e[10D\e[27md\e[27mu\e[27m \e[27m-\e[27mh\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[?2004l\r\r\n"
- delay: 7
content: "229M\ttrip/2021/01/03\r\n712K\ttrip/2021/01/02\r\n7.5M\ttrip/2021/01/01\r\n237M\ttrip/2021/01\r\n237M\ttrip/2021\r\n238M\ttrip\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J \e[K\e[?2004h"
- delay: 4280
content: "\e[7mfind trip | head -20\e[27m"
- delay: 923
content: "\e[20D\e[27mf\e[27mi\e[27mn\e[27md\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[27m \e[27m|\e[27m \e[27mh\e[27me\e[27ma\e[27md\e[27m \e[27m-\e[27m2\e[27m0\e[?2004l\r\r\n"
- delay: 5
content: "trip\r\ntrip/2021\r\ntrip/2021/01\r\ntrip/2021/01/03\r\ntrip/2021/01/03/IMG_1234 (1).HEIC\r\ntrip/2021/01/03/IMG_1267.HEIC\r\ntrip/2021/01/03/IMG_1226.HEIC\r\ntrip/2021/01/03/IMG_1271.HEIC\r\ntrip/2021/01/03/IMG_1232 (1).JPG\r\ntrip/2021/01/03/IMG_1270.HEIC\r\ntrip/2021/01/03/IMG_1231.HEIC\r\ntrip/2021/01/03/IMG_6926.JPG\r\ntrip/2021/01/03/IMG_6932.JPG\r\ntrip/2021/01/03/IMG_1266.HEIC\r\ntrip/2021/01/03/IMG_6933.JPG\r\ntrip/2021/01/03/IMG_6927.JPG\r\ntrip/2021/01/03/IMG_1233 (1).JPG\r\ntrip/2021/01/03/IMG_1228 (1).HEIC\r\ntrip/2021/01/03/IMG_6931.JPG\r\ntrip/2021/01/03/IMG_6930.JPG\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J \e[K\e[?2004h"
- delay: 3615
content: "\e[?2004l\r\r\n"

114
docs/search.html Normal file
View File

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

1
docs/searchindex.js Normal file

File diff suppressed because one or more lines are too long

28
docsrc/Makefile Normal file
View File

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

13
docsrc/README.md Normal file
View File

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

35
docsrc/make.bat Normal file
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

@@ -79,7 +79,7 @@ def export(export_path, default_album, library_path, edited):
exported = p.export(dest_dir, filename)
click.echo(f"Exported {filename} to {exported}")
else:
click.echo(f"Skipping missing photo: {p.original_filename} in album {album}")
click.echo(f"Skipping missing photo: {p.original_filename}")
if __name__ == "__main__":

83
examples/export_faces.py Normal file
View File

@@ -0,0 +1,83 @@
""" Export all photos that contain a detected face and draw rectangles around each face
photos with no persons/detected faces will not be export
This shows how to use the FaceInfo class and is useful for validating that FaceInfo is
correctly handling faces.
To use this, you'll need to install Pillow:
python3 -m pip install Pillow
"""
import os
import click
from PIL import Image, ImageDraw
import osxphotos
@click.command()
@click.argument("export-path", type=click.Path(exists=True))
@click.option(
"--uuid",
metavar="UUID",
help="Limit export to optional UUID(s)",
required=False,
multiple=True,
)
@click.option(
"--library-path",
metavar="PATH",
help="Path to Photos library, default to last used library",
default=None,
)
def export(export_path, library_path, uuid):
""" export photos to export_path and draw faces """
library_path = os.path.expanduser(library_path) if library_path else None
if library_path is not None:
photosdb = osxphotos.PhotosDB(library_path)
else:
photosdb = osxphotos.PhotosDB()
photos = photosdb.photos(uuid=uuid) if uuid else photosdb.photos(movies=False)
for p in photos:
if p.person_info and not p.ismissing:
# has persons and not missing
if "heic" in p.filename.lower():
print(f"skipping heic image {p.filename}")
continue
print(f"exporting photo {p.original_filename}, uuid = {p.uuid}")
export = p.export(export_path, p.original_filename, edited=p.hasadjustments)
if export:
im = Image.open(export[0])
draw = ImageDraw.Draw(im)
for face in p.face_info:
coords = face.face_rect()
draw.rectangle(coords, width=3)
draw.ellipse(get_circle_points(face.center, 3), width=1)
draw.text(face.mouth, "M", fill=(255, 255, 255, 255))
draw.text(face.left_eye, "L", fill=(255, 255, 255, 255))
draw.text(face.right_eye, "R", fill=(255, 255, 255, 255))
im.save(export[0])
else:
print(f"no photos exported for {p.uuid}")
def get_circle_points(xy, radius):
""" Returns tuples of (x0, y0), (x1, y1) for a circle centered at x, y with radius
Arguments:
xy: tuple of x, y coordinates
radius: radius of circle to draw
Returns:
[(x0, y0), (x1, y1)] for bounding box of circle centered at x, y
"""
x, y = xy
x0, y0 = x - radius, y - radius
x1, y1 = x + radius, y + radius
return [(x0, y0), (x1, y1)]
if __name__ == "__main__":
export() # pylint: disable=no-value-for-parameter

View File

@@ -0,0 +1,42 @@
""" use osxphotos to force the download of photos from iCloud
downloads images to a temporary directory then deletes them
resulting in the photo being downloaded to Photos library
"""
import os
import sys
import tempfile
import osxphotos
def main():
photosdb = osxphotos.PhotosDB()
tempdir = tempfile.TemporaryDirectory()
photos = photosdb.photos()
downloaded = 0
missing = [photo for photo in photos if photo.ismissing and not photo.shared]
if not missing:
print(f"Did not find any missing photos to download")
sys.exit(0)
print(f"Downloading {len(missing)} photos")
for photo in missing:
if photo.ismissing:
print(f"Downloading photo {photo.original_filename}")
downloaded += 1
exported = photo.export(tempdir.name, use_photos_export=True, timeout=300)
if photo.hasadjustments:
exported.extend(
photo.export(tempdir.name, use_photos_export=True, edited=True, timeout=300)
)
for filename in exported:
print(f"Removing temporary file {filename}")
os.unlink(filename)
print(f"Downloaded {downloaded} photos")
tempdir.cleanup()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,156 @@
""" get shared comments associated with a photo """
import datetime
import sys
from dataclasses import dataclass
import osxphotos
from osxphotos._constants import TIME_DELTA
@dataclass
class Comment:
""" Class for shared photo comments """
uuid: str
sort_fok: int
datetime: datetime.datetime
user: str
ismine: bool
text: str
@dataclass
class Like:
""" Class for shared photo likes """
uuid: str
sort_fok: int
datetime: datetime.datetime
user: str
ismine: bool
def get_shared_person_info(photosdb, hashed_person_id):
""" returns tuple of (first name, last name, full name)
for person invited to shared album with
ZINVITEEHASHEDPERSONID = hashed_person_id
Args:
photosdb: a osxphotos.PhotosDB object
hashed_person_id: str, value of ZINVITEEHASHEDPERSONID to lookup
"""
conn, _ = photosdb.get_db_connection()
results = conn.execute(
"""
SELECT
ZINVITEEHASHEDPERSONID,
ZINVITEEFIRSTNAME,
ZINVITEELASTNAME,
ZINVITEEFULLNAME
FROM
ZCLOUDSHAREDALBUMINVITATIONRECORD
WHERE
ZINVITEEHASHEDPERSONID = ?
LIMIT 1
""",
([hashed_person_id]),
).fetchall()
if results:
row = results[0]
return (row[1], row[2], row[3])
else:
return (None, None, None)
def get_comments(photosdb, uuid):
""" return comments and likes, if any, for photo with uuid
Args:
photosdb: a osxphotos.PhotosDB object
uuid: uuid of the photo
Returns:
tuple of (list of comments as Comment objects or [] if no comments, list of likes as Like objects or [] if no likes)
"""
conn, _ = photosdb.get_db_connection()
results = conn.execute(
"""
SELECT
ZGENERICASSET.ZUUID, --0: UUID of the photo
ZCLOUDSHAREDCOMMENT.ZISLIKE, --1: comment is actually a "like"
ZCLOUDSHAREDCOMMENT.Z_FOK_COMMENTEDASSET, --2: sort order for comments on a photo
ZCLOUDSHAREDCOMMENT.ZCOMMENTDATE, --3: date of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTTEXT, --4: text of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTERHASHEDPERSONID, --5: hashed ID of person who made comment/like
ZCLOUDSHAREDCOMMENT.ZISMYCOMMENT --6: is my (this user's) comment
FROM ZCLOUDSHAREDCOMMENT
JOIN ZGENERICASSET ON
ZGENERICASSET.Z_PK = ZCLOUDSHAREDCOMMENT.ZCOMMENTEDASSET
OR
ZGENERICASSET.Z_PK = ZCLOUDSHAREDCOMMENT.ZLIKEDASSET
WHERE ZGENERICASSET.ZUUID = ?
""",
([uuid]),
).fetchall()
comments = []
likes = []
for row in results:
photo_uuid = row[0]
sort_fok = row[2] or 0 # sort_fok is Null/None for likes
is_like = bool(row[1])
text = row[4]
user_info = get_shared_person_info(photosdb, row[5])
try:
dt = datetime.datetime.fromtimestamp(row[3] + TIME_DELTA)
except:
dt = datetime.datetime(1970, 1, 1)
ismine = bool(row[6])
if is_like:
# it's a like
likes.append(Like(photo_uuid, sort_fok, dt, user_info[2], ismine))
elif text:
# comment
comments.append(
Comment(photo_uuid, sort_fok, dt, user_info[2], ismine, text)
)
if likes:
likes.sort(key=lambda x: x.datetime)
if comments:
comments.sort(key=lambda x: x.sort_fok)
return (comments, likes)
def main():
if len(sys.argv) > 1:
# library as first argument
photosdb = osxphotos.PhotosDB(dbfile=sys.argv[1])
else:
# open default library
photosdb = osxphotos.PhotosDB()
# shared albums
shared_albums = photosdb.album_info_shared
for album in shared_albums:
print(f"Processing album {album.title}")
# only shared albums can have comments
for photo in album.photos:
comments, likes = get_comments(photosdb, photo.uuid)
if comments or likes:
print(f"{photo.uuid}, {photo.original_filename}: ")
if likes:
print("Likes:")
for like in likes:
print(like)
if comments:
print("Comments:")
for comment in comments:
print(comment)
if __name__ == "__main__":
main()

View File

@@ -15,7 +15,7 @@ import time
import click
import osxphotos
from osxphotos.__main__ import get_photos_db, _list_libraries
from osxphotos.cli import get_photos_db, _list_libraries
def show(photo):
@@ -42,7 +42,7 @@ def main():
if db:
print("loading database")
tic = time.perf_counter()
photosdb = osxphotos.PhotosDB(dbfile=db)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=print)
toc = time.perf_counter()
print(f"done: took {toc-tic} seconds")
return photosdb
@@ -58,5 +58,6 @@ if __name__ == "__main__":
print("getting photos")
tic = time.perf_counter()
photos = photosdb.photos(images=True, movies=True)
photos.extend(photosdb.photos(images=True, movies=True, intrash=True))
toc = time.perf_counter()
print(f"found {len(photos)} photos in {toc-tic} seconds")

View File

@@ -5,4 +5,4 @@
# If you need to install pyinstaller:
# python3 -m pip install --upgrade pyinstaller
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
pyinstaller osxphotos.spec

48
osxphotos.spec Normal file
View File

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

View File

@@ -1,8 +1,7 @@
import logging
from ._version import __version__
from .photoinfo import PhotoInfo
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototemplate import PhotoTemplate
from .utils import _debug, _get_logger, _set_debug

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,16 @@ Constants used by osxphotos
"""
import os.path
from datetime import datetime
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
# Time delta: add this to Photos times to get unix time
# Apple Epoch is Jan 1, 2001
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
# Unicode format to use for comparing strings
UNICODE_FORMAT = "NFC"
# which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622
@@ -10,18 +20,57 @@ import os.path
# Photos 4.0 (10.14.5) == 4016
# Photos 4.0 (10.14.6) == 4025
# Photos 5.0 (10.15.0) == 6000
# TODO: Should this also use compatibleBackToVersion from LiGlobals?
_TESTED_DB_VERSIONS = ["6000", "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
# Photos 5 (10.15.1) == 13537
# Photos 5 (10.15.4, 10.15.5, 10.15.6) == 13703
# Photos 6 (10.16.0 Beta) == 14104
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
# 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.5
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.6
# which major version operating systems have been tested
_TESTED_OS_VERSIONS = ["12", "13", "14", "15"]
# Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
# some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = {
5: {
"ASSET": "ZGENERICASSET",
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_37KEYWORDS",
"ALBUM_JOIN": "Z_26ASSETS.Z_34ASSETS",
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
"DEPTH_STATE": "ZGENERICASSET.ZDEPTHSTATES",
},
6: {
"ASSET": "ZASSET",
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_36KEYWORDS",
"ALBUM_JOIN": "Z_26ASSETS.Z_3ASSETS",
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
},
}
# which version operating systems have been tested
_TESTED_OS_VERSIONS = [
("10", "12"),
("10", "13"),
("10", "14"),
("10", "15"),
("10", "16"),
("11", "0"),
("11", "1"),
("11", "2"),
]
# Photos 5 has persons who are empty string if unidentified face
_UNKNOWN_PERSON = "_UNKNOWN_"
@@ -41,12 +90,14 @@ _MOVIE_TYPE = 1
# Name of XMP template file
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
_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_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"
@@ -63,3 +114,99 @@ _OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
# SearchInfo categories for Photos 5, corresponds to categories in database/search/psi.sqlite
SEARCH_CATEGORY_LABEL = 2024
SEARCH_CATEGORY_PLACE_NAME = 1
SEARCH_CATEGORY_STREET = 2
SEARCH_CATEGORY_NEIGHBORHOOD = 3
SEARCH_CATEGORY_LOCALITY_4 = 4
SEARCH_CATEGORY_SUB_LOCALITY_5 = 5
SEARCH_CATEGORY_SUB_LOCALITY_6 = 6
SEARCH_CATEGORY_CITY = 7
SEARCH_CATEGORY_LOCALITY_8 = 8
SEARCH_CATEGORY_NAMED_AREA = 9
SEARCH_CATEGORY_ALL_LOCALITY = [
SEARCH_CATEGORY_LOCALITY_4,
SEARCH_CATEGORY_SUB_LOCALITY_5,
SEARCH_CATEGORY_SUB_LOCALITY_6,
SEARCH_CATEGORY_LOCALITY_8,
SEARCH_CATEGORY_NAMED_AREA,
]
SEARCH_CATEGORY_STATE = 10
SEARCH_CATEGORY_STATE_ABBREVIATION = 11
SEARCH_CATEGORY_COUNTRY = 12
SEARCH_CATEGORY_BODY_OF_WATER = 14
SEARCH_CATEGORY_MONTH = 1014
SEARCH_CATEGORY_YEAR = 1015
SEARCH_CATEGORY_KEYWORDS = 2016
SEARCH_CATEGORY_TITLE = 2017
SEARCH_CATEGORY_DESCRIPTION = 2018
SEARCH_CATEGORY_HOME = 2020
SEARCH_CATEGORY_PERSON = 2021
SEARCH_CATEGORY_ACTIVITY = 2027
SEARCH_CATEGORY_HOLIDAY = 2029
SEARCH_CATEGORY_SEASON = 2030
SEARCH_CATEGORY_WORK = 2036
SEARCH_CATEGORY_VENUE = 2038
SEARCH_CATEGORY_VENUE_TYPE = 2039
SEARCH_CATEGORY_PHOTO_TYPE_VIDEO = 2044
SEARCH_CATEGORY_PHOTO_TYPE_SLOMO = 2045
SEARCH_CATEGORY_PHOTO_TYPE_LIVE = 2046
SEARCH_CATEGORY_PHOTO_TYPE_SCREENSHOT = 2047
SEARCH_CATEGORY_PHOTO_TYPE_PANORAMA = 2048
SEARCH_CATEGORY_PHOTO_TYPE_TIMELAPSE = 2049
SEARCH_CATEGORY_PHOTO_TYPE_BURSTS = 2052
SEARCH_CATEGORY_PHOTO_TYPE_PORTRAIT = 2053
SEARCH_CATEGORY_PHOTO_TYPE_SELFIES = 2054
SEARCH_CATEGORY_PHOTO_TYPE_FAVORITES = 2055
SEARCH_CATEGORY_MEDIA_TYPES = [
SEARCH_CATEGORY_PHOTO_TYPE_VIDEO,
SEARCH_CATEGORY_PHOTO_TYPE_SLOMO,
SEARCH_CATEGORY_PHOTO_TYPE_LIVE,
SEARCH_CATEGORY_PHOTO_TYPE_SCREENSHOT,
SEARCH_CATEGORY_PHOTO_TYPE_PANORAMA,
SEARCH_CATEGORY_PHOTO_TYPE_TIMELAPSE,
SEARCH_CATEGORY_PHOTO_TYPE_BURSTS,
SEARCH_CATEGORY_PHOTO_TYPE_PORTRAIT,
SEARCH_CATEGORY_PHOTO_TYPE_SELFIES,
SEARCH_CATEGORY_PHOTO_TYPE_FAVORITES,
]
SEARCH_CATEGORY_PHOTO_NAME = 2056
# Max filename length on MacOS
MAX_FILENAME_LEN = 255
# Max directory name length on MacOS
MAX_DIRNAME_LEN = 255
# Default JPEG quality when converting to JPEG
DEFAULT_JPEG_QUALITY = 1.0
# Default suffix to add to edited images
DEFAULT_EDITED_SUFFIX = "_edited"
# Default suffix to add to original images
DEFAULT_ORIGINAL_SUFFIX = ""
# Colors for print CLI messages
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
# Bit masks for --sidecar
SIDECAR_JSON = 0x1
SIDECAR_EXIFTOOL = 0x2
SIDECAR_XMP = 0x4
# supported attributes for --xattr-template
EXTENDED_ATTRIBUTE_NAMES = [
"authors",
"comment",
"copyright",
"description",
"findercomment",
"headline",
"keywords",
]
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
# name of export DB
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.30.2"
__version__ = "0.41.0"

View File

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

View File

@@ -10,7 +10,7 @@ Represents a single Folder in the Photos library and provides access to the fold
PhotosDB.folders() returns a list of FolderInfo objects
"""
import logging
from datetime import datetime, timedelta, timezone
from ._constants import (
_PHOTOS_4_ALBUM_KIND,
@@ -18,11 +18,34 @@ from ._constants import (
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
TIME_DELTA,
)
from .datetime_utils import get_local_tz
class AlbumInfo:
def sort_list_by_keys(values, sort_keys):
""" Sorts list values by a second list sort_keys
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
Args:
values: a list of values to be sorted
sort_keys: a list of keys to sort values by
Returns:
list of values, sorted by sort_keys
Raises:
ValueError: raised if len(values) != len(sort_keys)
"""
if len(values) != len(sort_keys):
return ValueError("values and sort_keys must have same length")
return list(zip(*sorted(zip(sort_keys, values))))[1]
class AlbumInfoBaseClass:
"""
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
@@ -31,25 +54,111 @@ class AlbumInfo:
self._uuid = uuid
self._db = db
self._title = self._db._dbalbum_details[uuid]["title"]
@property
def title(self):
""" return title / name of album """
return self._title
self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"]
self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"]
self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"]
self._local_tz = get_local_tz(
datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
)
@property
def uuid(self):
""" return uuid of album """
return self._uuid
@property
def creation_date(self):
""" return creation date of album """
try:
return self._creation_date
except AttributeError:
try:
self._creation_date = (
datetime.fromtimestamp(
self._creation_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._creation_date_timestamp
else datetime(1970, 1, 1, 0, 0, 0).astimezone(
tz=timezone(timedelta(0))
)
)
except ValueError:
self._creation_date = datetime(1970, 1, 1, 0, 0, 0).astimezone(
tz=timezone(timedelta(0))
)
return self._creation_date
@property
def start_date(self):
""" For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began) """
try:
return self._start_date
except AttributeError:
try:
self._start_date = (
datetime.fromtimestamp(
self._start_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._start_date_timestamp
else None
)
except ValueError:
self._start_date = None
return self._start_date
@property
def end_date(self):
""" For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed) """
try:
return self._end_date
except AttributeError:
try:
self._end_date = (
datetime.fromtimestamp(
self._end_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._end_date_timestamp
else None
)
except ValueError:
self._end_date = None
return self._end_date
@property
def photos(self):
""" return list of photos contained in album """
return []
def __len__(self):
""" return number of photos contained in album """
return len(self.photos)
class AlbumInfo(AlbumInfoBaseClass):
"""
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
@property
def title(self):
""" return title / name of album """
return self._title
@property
def photos(self):
""" return list of photos contained in album sorted in same sort order as Photos """
try:
return self._photos
except AttributeError:
uuid = self._db._dbalbums_album[self._uuid]
self._photos = self._db.photos(uuid=uuid)
if self.uuid in self._db._dbalbums_album:
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
sorted_uuid = sort_list_by_keys(uuid, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
else:
self._photos = []
return self._photos
@property
@@ -100,9 +209,24 @@ class AlbumInfo:
)
return self._parent
def __len__(self):
""" return number of photos contained in album """
return len(self.photos)
class ImportInfo(AlbumInfoBaseClass):
@property
def photos(self):
""" return list of photos contained in import session """
try:
return self._photos
except AttributeError:
uuid_list, sort_order = zip(
*[
(uuid, self._db._dbphotos[uuid]["fok_import_session"])
for uuid in self._db._dbphotos
if self._db._dbphotos[uuid]["import_uuid"] == self.uuid
]
)
sorted_uuid = sort_list_by_keys(uuid_list, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
return self._photos
class FolderInfo:

3604
osxphotos/cli.py Normal file

File diff suppressed because it is too large Load Diff

197
osxphotos/cli_help.py Normal file
View File

@@ -0,0 +1,197 @@
"""Help text helper class for osxphotos CLI """
import io
import re
import click
import osxmetadata
from rich.console import Console
from rich.markdown import Markdown
from ._constants import (
EXTENDED_ATTRIBUTE_NAMES,
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
OSXPHOTOS_EXPORT_DB,
)
from .phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
get_template_help,
)
class ExportCommand(click.Command):
""" Custom click.Command that overrides get_help() to show additional help info for export """
def get_help(self, ctx):
help_text = super().get_help(ctx)
formatter = click.HelpFormatter()
# passed to click.HelpFormatter.write_dl for formatting
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
formatter.write("\n")
formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level "
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
+ "used for determining which files need to be updated when run with --update. It is recommended "
+ "that if you later move the export folder tree you also move the database file."
)
formatter.write("\n")
formatter.write_text(
"The --update option will only copy new or updated files from the library "
+ "to the export folder. If a file is changed in the export folder (for example, you edited the "
+ "exported image), osxphotos will detect this as a difference and re-export the original image "
+ "from the library thus overwriting the changes. If using --update, the exported library "
+ "should be treated as a backup, not a working copy where you intend to make changes. "
+ "If you do edit or process the exported files and do not want them to be overwritten with"
+ "subsequent --update, use --ignore-signature which will match filename but not file signature when "
+ "exporting."
)
formatter.write("\n")
formatter.write_text(
"Note: The number of files reported for export and the number actually exported "
+ "may differ due to live photos, associated raw images, and edited photos which are reported "
+ "in the total photos exported."
)
formatter.write("\n")
formatter.write_text(
"Implementation note: To determine which files need to be updated, "
+ f"osxphotos stores file signature information in the '{OSXPHOTOS_EXPORT_DB}' database. "
+ "The signature includes size, modification time, and filename. In order to minimize "
+ "run time, --update does not do a full comparison (diff) of the files nor does it compare "
+ "hashes of the files. In normal usage, this is sufficient for updating the library. "
+ "You can always run export without the --update option to re-export the entire library thus "
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
)
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width))
formatter.write("\n")
formatter.write_text(
"""
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
additional metadata to extended attributes in the file. These options will only work
if the destination filesystem supports extended attributes (most do).
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services
do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes
will cause Dropbox to re-sync the files.
The following attributes may be used with '--xattr-template':
"""
)
formatter.write_dl(
[
(
attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
)
for attr in EXTENDED_ATTRIBUTE_NAMES
]
)
formatter.write("\n")
formatter.write_text(
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
)
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Templating System **[/bold]", width=formatter.width))
formatter.write("\n")
formatter.write(template_help(width=formatter.width))
formatter.write("\n")
formatter.write_text(
"With the --directory and --filename options you may specify a template for the "
+ "export directory or filename, respectively. "
+ "The directory will be appended to the export path specified "
+ "in the export DEST argument to export. For example, if template is "
+ "'{created.year}/{created.month}', and export destination DEST is "
+ "'/Users/maria/Pictures/export', "
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
+ "if the photo was created in March 2020. "
)
formatter.write("\n")
formatter.write_text(
"The templating system may also be used with the --keyword-template option "
+ "to set keywords on export (with --exiftool or --sidecar), "
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}" '
+ "or in the 'folder>subfolder>album' format used in Lightroom Classic, --keyword-template \"{folder_album(>)}\"."
)
formatter.write("\n")
formatter.write_text(
"In the template, valid template substitutions will be replaced by "
+ "the corresponding value from the table below. Invalid substitutions will result in a "
+ "an error and the script will abort."
)
formatter.write("\n")
formatter.write(rich_text("[bold]** Template Substitutions **[/bold]", width=formatter.width))
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
formatter.write_dl(templ_tuples)
formatter.write("\n")
formatter.write_text(
"The following substitutions may result in multiple values. Thus "
+ "if specified for --directory these could result in multiple copies of a photo being "
+ "being exported, one to each directory. For example: "
+ "--directory '{created.year}/{album}' could result in the same photo being exported "
+ "to each of the following directories if the photos were created in 2019 "
+ "and were in albums 'Vacation' and 'Family': "
+ "2019/Vacation, 2019/Family"
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend(
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
)
formatter.write_dl(templ_tuples)
help_text += formatter.getvalue()
return help_text
def template_help(width=78):
"""Return formatted string for template system """
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
template_help_md = strip_md_links(get_template_help())
console.print(Markdown(template_help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def rich_text(text, width=78):
"""Return rich formatted text"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
console.print(text)
rich_text = sio.getvalue()
sio.close()
return rich_text
def strip_md_links(md):
"""strip markdown links from markdown text md
Args:
md: str, markdown text
Returns:
str with markdown links removed
Note: This uses a very basic regex that likely fails on all sorts of edge cases
but works for the links in the osxphotos docs
"""
links = r"(?:[*#])|\[(.*?)\]\(.+?\)"
def subfn(match):
return match.group(1)
return re.sub(links, subfn, md)

173
osxphotos/configoptions.py Normal file
View File

@@ -0,0 +1,173 @@
""" ConfigOptions class to load/save config settings for osxphotos CLI """
import toml
class ConfigOptionsException(Exception):
""" Invalid combination of options. """
def __init__(self, message):
self.message = message
super().__init__(self.message)
class ConfigOptionsInvalidError(ConfigOptionsException):
pass
class ConfigOptionsLoadError(ConfigOptionsException):
pass
class ConfigOptions:
""" data class to store and load options for osxphotos commands """
def __init__(self, name, attrs, ignore=None):
""" init ConfigOptions class
Args:
name: name for these options, will be used for section heading in TOML file when saving/loading from file
attrs: dict with name and default value for all allowed attributes
ignore: optional list of strings of keys to ignore from attrs dict
"""
self._name = name
self._attrs = attrs.copy()
if ignore:
for attrname in ignore:
self._attrs.pop(attrname, None)
self.set_attributes(attrs)
def set_attributes(self, args):
for attr in self._attrs:
try:
arg = args[attr]
# don't test 'not arg'; need to handle empty strings as valid values
if arg is None or arg == False:
if type(self._attrs[attr]) == tuple:
setattr(self, attr, ())
else:
setattr(self, attr, self._attrs[attr])
else:
setattr(self, attr, arg)
except KeyError:
raise KeyError(f"Missing argument: {attr}")
def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False):
""" 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;
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"))...]
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;
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
"""
if not any([exclusive, inclusive, dependent]):
return True
prefix = "--" if cli else ""
if exclusive:
for a, b in exclusive:
vala = getattr(self, a)
valb = getattr(self, b)
vala = any(vala) if isinstance(vala, tuple) else vala
valb = any(valb) if isinstance(valb, tuple) else valb
if vala and valb:
stra = a.replace("_", "-") if cli else a
strb = b.replace("_", "-") if cli else b
raise ConfigOptionsInvalidError(
f"{prefix}{stra} and {prefix}{strb} options cannot be used together."
)
if inclusive:
for a, b in inclusive:
vala = getattr(self, a)
valb = getattr(self, b)
vala = any(vala) if isinstance(vala, tuple) else vala
valb = any(valb) if isinstance(valb, tuple) else valb
if any([vala, valb]) and not all([vala, valb]):
stra = a.replace("_", "-") if cli else a
strb = b.replace("_", "-") if cli else b
raise ConfigOptionsInvalidError(
f"{prefix}{stra} and {prefix}{strb} options must be used together."
)
if dependent:
for a, b in dependent:
vala = getattr(self, a)
if not isinstance(b, tuple):
# python unrolls the tuple if there's a single element
b = (b,)
valb = [getattr(self, x) for x in b]
valb = [any(x) if isinstance(x, tuple) else x for x in valb]
if vala and not any(valb):
if cli:
stra = prefix + a.replace("_", "-")
strb = ", ".join(prefix + x.replace("_", "-") for x in b)
else:
stra = a
strb = ", ".join(b)
raise ConfigOptionsInvalidError(
f"{stra} must be used with at least one of: {strb}."
)
return True
def write_to_file(self, filename):
""" 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)
def load_from_file(self, filename, override=False):
""" Load options from a TOML file.
Args:
filename: full path to TOML file
override: bool; if True, values in the TOML file will override values already set in the instance
Raises:
ConfigOptionsLoadError if there are any errors during the parsing of the TOML file
"""
loaded = toml.load(filename)
name = self._name
if name not in loaded:
raise ConfigOptionsLoadError(f"[{name}] section missing from {filename}")
for attr in loaded[name]:
if attr not in self._attrs:
raise ConfigOptionsLoadError(
f"Unknown option: {attr} = {loaded[name][attr]}"
)
val = loaded[name][attr]
if not override:
# use value from self if set
val = getattr(self, attr) or val
if type(self._attrs[attr]) == tuple:
val = tuple(val)
setattr(self, attr, val)
return self
def asdict(self):
return {attr: getattr(self, attr) for attr in sorted(self._attrs.keys())}

161
osxphotos/datetime_utils.py Normal file
View File

@@ -0,0 +1,161 @@
""" datetime.datetime helper functions for converting to/from UTC """
import datetime
def get_local_tz(dt):
""" Return local timezone as datetime.timezone tzinfo for dt
Args:
dt: datetime.datetime
Returns:
local timezone for dt as datetime.timezone
Raises:
ValueError if dt is not timezone naive
"""
if not datetime_has_tz(dt):
return dt.astimezone().tzinfo
else:
raise ValueError("dt must be naive datetime.datetime object")
def datetime_has_tz(dt):
""" 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
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def datetime_tz_to_utc(dt):
""" Convert datetime.datetime object with timezone to UTC timezone
Args:
dt: datetime.datetime object
Returns:
datetime.datetime in UTC timezone
Raises:
TypeError if dt is not datetime.datetime object
ValueError if dt does not have timeone information
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
return dt.replace(tzinfo=dt.tzinfo).astimezone(tz=datetime.timezone.utc)
else:
raise ValueError(f"dt does not have timezone info")
def datetime_remove_tz(dt):
""" Remove timezone from a datetime.datetime object
Args:
dt: datetime.datetime object with tzinfo
Returns:
dt without any timezone info (naive datetime object)
Raises:
TypeError if dt is not a datetime.datetime object
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
return dt.replace(tzinfo=None)
def datetime_naive_to_utc(dt):
""" 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
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
# has timezone info
raise ValueError(
"dt must be naive/timezone unaware: "
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tzinfo.utcoffset(dt)}"
)
return dt.replace(tzinfo=datetime.timezone.utc)
def datetime_naive_to_local(dt):
""" 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
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
# has timezone info
raise ValueError(
"dt must be naive/timezone unaware: "
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tizinfo.utcoffset(dt)}"
)
return dt.replace(tzinfo=get_local_tz(dt))
def datetime_utc_to_local(dt):
""" Convert datetime.datetime object in UTC timezone to local timezone
Args:
dt: datetime.datetime object
Returns:
datetime.datetime in local timezone
Raises:
TypeError if dt is not a datetime.datetime object
ValueError if dt is not in UTC timezone
"""
if type(dt) != datetime.datetime:
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
if dt.tzinfo is not datetime.timezone.utc:
raise ValueError(f"{dt} must be in UTC timezone: timezone = {dt.tzinfo}")
return dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None)

View File

@@ -2,18 +2,18 @@
I rolled my own for following reasons:
1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)
2. I wanted singleton behavior so only a single exiftool process was ever running
3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance)
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
import json
import logging
import os
import re
import shutil
import subprocess
import sys
from functools import lru_cache # pylint: disable=syntax-error
from .utils import _debug
# exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}"
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
@@ -22,10 +22,7 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
result = subprocess.run(["which", "exiftool"], stdout=subprocess.PIPE)
exiftool_path = result.stdout.decode("utf-8")
if _debug():
logging.debug("exiftool path = %s" % (exiftool_path))
exiftool_path = shutil.which("exiftool")
if exiftool_path:
return exiftool_path.rstrip()
else:
@@ -36,8 +33,8 @@ def get_exiftool_path():
class _ExifToolProc:
""" Runs exiftool in a subprocess via Popen
Creates a singleton object """
"""Runs exiftool in a subprocess via Popen
Creates a singleton object"""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
@@ -47,20 +44,20 @@ class _ExifToolProc:
return cls.instance
def __init__(self, exiftool=None):
""" construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it) """
"""construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it)"""
if hasattr(self, "_process_running") and self._process_running:
# already running
if exiftool is not None:
if exiftool is not None and exiftool != self._exiftool:
logging.warning(
f"exiftool subprocess already running, "
f"ignoring exiftool={exiftool}"
)
return
self._exiftool = exiftool if exiftool else get_exiftool_path()
self._process_running = False
self._exiftool = exiftool or get_exiftool_path()
self._start_proc()
@property
@@ -98,18 +95,19 @@ class _ExifToolProc:
"-", # read from stdin
"-common_args", # specifies args common to all commands subsequently run
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-P", # Preserve file modification date/time
"-G", # print group name for each tag
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
self._process_running = True
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
if not self._process_running:
logging.warning("exiftool process is not running")
return
self._process.stdin.write(b"-stay_open\n")
@@ -134,39 +132,79 @@ class _ExifToolProc:
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
def __init__(self, filepath, exiftool=None, overwrite=True):
""" Return ExifTool object
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
"""Create ExifTool object
Args:
file: path to image file
exiftool: path to exiftool, if not specified will look in path
overwrite: if True, will overwrite image file without creating backup, default=False """
overwrite: if True, will overwrite image file without creating backup, default=False
flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)
Returns:
ExifTool instance
"""
self.file = filepath
self.overwrite = overwrite
self.flags = flags or []
self.data = {}
self.warning = None
self.error = None
# if running as a context manager, self._context_mgr will be True
self._context_mgr = False
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
self._process = self._exiftoolproc.process
self._read_exif()
def setvalue(self, tag, value):
""" Set tag to value(s)
if value is None, will delete tag """
"""Set tag to value(s); if value is None, will delete tag
Args:
tag: str; name of tag to set
value: str; value to set tag to
Returns:
True if success otherwise False
If error generated by exiftool, returns False and sets self.error to error string
If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string
If called in context manager, returns True (execution is delayed until exiting context manager)
"""
if value is None:
value = ""
command = [f"-{tag}={value}"]
if self.overwrite:
if self.overwrite and not self._context_mgr:
command.append("-overwrite_original")
self.run_commands(*command)
if self._context_mgr:
self._commands.extend(command)
return True
else:
_, _, error = self.run_commands(*command)
return error is None
def addvalues(self, tag, *values):
""" Add one or more value(s) to tag
"""Add one or more value(s) to tag
If more than one value is passed, each value will be added to the tag
Notes: exiftool may add duplicate values for some tags so the caller must ensure
the values being added are not already in the EXIF data
For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,
but for others, such as EXIF:ISO, this will literally add a value to the existing value.
It's up to the caller to know what exiftool will do for each tag
If setvalue called before addvalues, exiftool does not appear to add duplicates,
but if addvalues called without first calling setvalue, exiftool will add duplicate values
Args:
tag: str; tag to set
*values: str; one or more values to set
Returns:
True if success otherwise False
If error generated by exiftool, returns False and sets self.error to error string
If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string
If called in context manager, returns True (execution is delayed until exiting context manager)
Notes: exiftool may add duplicate values for some tags so the caller must ensure
the values being added are not already in the EXIF data
For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,
but for others, such as EXIF:ISO, this will literally add a value to the existing value.
It's up to the caller to know what exiftool will do for each tag
If setvalue called before addvalues, exiftool does not appear to add duplicates,
but if addvalues called without first calling setvalue, exiftool will add duplicate values
"""
if not values:
raise ValueError("Must pass at least one value")
@@ -177,25 +215,55 @@ class ExifTool:
raise ValueError("Can't add None value to tag")
command.append(f"-{tag}+={value}")
if self.overwrite:
if self.overwrite and not self._context_mgr:
command.append("-overwrite_original")
if command:
self.run_commands(*command)
if self._context_mgr:
self._commands.extend(command)
return True
else:
_, _, error = self.run_commands(*command)
return error is None
def run_commands(self, *commands, no_file=False):
""" run commands in the exiftool process and return result
no_file: (bool) do not pass the filename to exiftool (default=False)
by default, all commands will be run against self.file
use no_file=True to run a command without passing the filename """
"""Run commands in the exiftool process and return result.
Args:
*commands: exiftool commands to run
no_file: (bool) do not pass the filename to exiftool (default=False)
by default, all commands will be run against self.file
use no_file=True to run a command without passing the filename
Returns:
(output, warning, errror)
output: bytes is containing output of exiftool commands
warning: if exiftool generated warnings, string containing warning otherwise empty string
error: if exiftool generated errors, string containing otherwise empty string
Note: Also sets self.warning and self.error if warning or error generated.
"""
if not (hasattr(self, "_process") and self._process):
raise ValueError("exiftool process is not running")
if not commands:
raise TypeError("must provide one or more command to run")
if self._context_mgr and self.overwrite:
commands = list(commands)
commands.append("-overwrite_original")
filename = os.fsencode(self.file) if not no_file else b""
command_str = (
if self.flags:
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
flags = []
for f in self.flags:
flags.extend(f.split())
command_str = b"\n".join([f.encode("utf-8") for f in flags])
command_str += b"\n"
else:
command_str = b""
command_str += (
b"\n".join([c.encode("utf-8") for c in commands])
+ b"\n"
+ filename
@@ -203,18 +271,27 @@ class ExifTool:
+ b"-execute\n"
)
if _debug():
logging.debug(command_str)
# send the command
self._process.stdin.write(command_str)
self._process.stdin.flush()
# read the output
output = b""
warning = b""
error = b""
while EXIFTOOL_STAYOPEN_EOF not in str(output):
output += self._process.stdout.readline().strip()
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN]
line = self._process.stdout.readline()
if line.startswith(b"Warning"):
warning += line.strip()
elif line.startswith(b"Error"):
error += line.strip()
else:
output += line.strip()
warning = "" if warning == b"" else warning.decode("utf-8")
error = "" if error == b"" else error.decode("utf-8")
self.warning = warning
self.error = error
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN], warning, error
@property
def pid(self):
@@ -224,28 +301,58 @@ class ExifTool:
@property
def version(self):
""" returns exiftool version """
ver = self.run_commands("-ver", no_file=True)
ver, _, _ = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
def as_dict(self):
""" return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
def asdict(self, tag_groups=True):
"""return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
Args:
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
"""
json_str = self.run_commands("-json")
if json_str:
exifdict = json.loads(json_str)
return exifdict[0]
else:
json_str, _, _ = self.run_commands("-json")
if not json_str:
return dict()
try:
exifdict = json.loads(json_str)
except Exception as e:
# will fail with some commands, e.g --ext AVI which produces
# 'No file with specified extension' instead of json
return dict()
exifdict = exifdict[0]
if not tag_groups:
# strip tag groups
exif_new = {}
for k, v in exifdict.items():
k = re.sub(r".*:", "", k)
exif_new[k] = v
exifdict = exif_new
return exifdict
def json(self):
""" returns JSON string containing all EXIF tags and values from exiftool """
return self.run_commands("-json")
json, _, _ = self.run_commands("-json")
return json
def _read_exif(self):
""" read exif data from file """
data = self.as_dict()
data = self.asdict()
self.data = {k: v for k, v in data.items()}
def __str__(self):
return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
def __enter__(self):
self._context_mgr = True
self._commands = []
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
return False
elif self._commands:
# run_commands sets self.warning and self.error as needed
self.run_commands(*self._commands)

View File

@@ -14,7 +14,7 @@ from sqlite3 import Error
from ._version import __version__
OSXPHOTOS_EXPORTDB_VERSION = "1.0"
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
class ExportDB_ABC(ABC):
@@ -36,6 +36,22 @@ class ExportDB_ABC(ABC):
def get_stat_orig_for_file(self, filename):
pass
@abstractmethod
def set_stat_edited_for_file(self, filename, stats):
pass
@abstractmethod
def get_stat_edited_for_file(self, filename):
pass
@abstractmethod
def set_stat_converted_for_file(self, filename, stats):
pass
@abstractmethod
def get_stat_converted_for_file(self, filename):
pass
@abstractmethod
def set_stat_exif_for_file(self, filename, stats):
pass
@@ -61,13 +77,40 @@ class ExportDB_ABC(ABC):
pass
@abstractmethod
def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
pass
@abstractmethod
def get_sidecar_for_file(self, filename):
pass
@abstractmethod
def get_previous_uuids(self):
pass
@abstractmethod
def set_data(
self,
filename,
uuid,
orig_stat,
exif_stat,
converted_stat,
edited_stat,
info_json,
exif_json,
):
pass
class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """
def __init__(self):
self.was_created = True
self.was_upgraded = False
self.version = OSXPHOTOS_EXPORTDB_VERSION
def get_uuid_for_file(self, filename):
pass
@@ -80,6 +123,18 @@ class ExportDBNoOp(ExportDB_ABC):
def get_stat_orig_for_file(self, filename):
pass
def set_stat_edited_for_file(self, filename, stats):
pass
def get_stat_edited_for_file(self, filename):
pass
def set_stat_converted_for_file(self, filename, stats):
pass
def get_stat_converted_for_file(self, filename):
pass
def set_stat_exif_for_file(self, filename, stats):
pass
@@ -98,7 +153,26 @@ class ExportDBNoOp(ExportDB_ABC):
def set_exifdata_for_file(self, uuid, exifdata):
pass
def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
pass
def get_sidecar_for_file(self, filename):
return None, (None, None, None)
def get_previous_uuids(self):
return []
def set_data(
self,
filename,
uuid,
orig_stat,
exif_stat,
converted_stat,
edited_stat,
info_json,
exif_json,
):
pass
@@ -122,7 +196,6 @@ class ExportDB(ExportDB_ABC):
returns None if filename not found in database
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
logging.debug(f"get_uuid: {filename}")
conn = self._conn
try:
c = conn.cursor()
@@ -135,14 +208,12 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
uuid = None
logging.debug(f"get_uuid: {uuid}")
return uuid
def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
logging.debug(f"set_uuid: {filename} {uuid}")
conn = self._conn
try:
c = conn.cursor()
@@ -162,7 +233,6 @@ class ExportDB(ExportDB_ABC):
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
logging.debug(f"set_stat_orig_for_file: {filename} {stats}")
conn = self._conn
try:
c = conn.cursor()
@@ -189,14 +259,30 @@ class ExportDB(ExportDB_ABC):
(filename,),
)
results = c.fetchone()
stats = results[0:3] if results else None
if results:
stats = results[0:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
stats = (None, None, None)
except Error as e:
logging.warning(e)
stats = (None, None, None)
logging.debug(f"get_stat_orig_for_file: {stats}")
return stats
def set_stat_edited_for_file(self, filename, stats):
""" set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
return self._set_stat_for_file("edited", filename, stats)
def get_stat_edited_for_file(self, filename):
""" get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
return self._get_stat_for_file("edited", filename)
def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
@@ -205,7 +291,6 @@ class ExportDB(ExportDB_ABC):
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
logging.debug(f"set_stat_exif_for_file: {filename} {stats}")
conn = self._conn
try:
c = conn.cursor()
@@ -232,14 +317,30 @@ class ExportDB(ExportDB_ABC):
(filename,),
)
results = c.fetchone()
stats = results[0:3] if results else None
if results:
stats = results[0:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
stats = (None, None, None)
except Error as e:
logging.warning(e)
stats = (None, None, None)
logging.debug(f"get_stat_exif_for_file: {stats}")
return stats
def set_stat_converted_for_file(self, filename, stats):
""" set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
return self._set_stat_for_file("converted", filename, stats)
def get_stat_converted_for_file(self, filename):
""" get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""
return self._get_stat_for_file("converted", filename)
def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """
conn = self._conn
@@ -252,7 +353,6 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
info = None
logging.debug(f"get_info: {uuid}, {info}")
return info
def set_info_for_uuid(self, uuid, info):
@@ -268,8 +368,6 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
logging.debug(f"set_info: {uuid}, {info}")
def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
@@ -286,7 +384,6 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
exifdata = None
logging.debug(f"get_exifdata: {filename}, {exifdata}")
return exifdata
def set_exifdata_for_file(self, filename, exifdata):
@@ -303,9 +400,72 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
logging.debug(f"set_exifdata: {filename}, {exifdata}")
def get_sidecar_for_file(self, filename):
""" returns the sidecar data and signature for a file """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT sidecar_data, mode, size, mtime FROM sidecar WHERE filepath_normalized = ?",
(filename,),
)
results = c.fetchone()
if results:
sidecar_data = results[0]
sidecar_sig = (
results[1],
results[2],
int(results[3]) if results[3] is not None else None,
)
else:
sidecar_data = None
sidecar_sig = (None, None, None)
except Error as e:
logging.warning(e)
sidecar_data = None
sidecar_sig = (None, None, None)
def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
return sidecar_data, sidecar_sig
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
""" sets the sidecar data and signature for a file """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO sidecar(filepath_normalized, sidecar_data, mode, size, mtime) VALUES (?, ?, ?, ?, ?);",
(filename, sidecar_data, *sidecar_sig),
)
conn.commit()
except Error as e:
logging.warning(e)
def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database """
conn = self._conn
previous_uuids = []
try:
c = conn.cursor()
c.execute("SELECT DISTINCT uuid FROM files")
results = c.fetchall()
previous_uuids = [row[0] for row in results]
except Error as e:
logging.warning(e)
return previous_uuids
def set_data(
self,
filename,
uuid,
orig_stat,
exif_stat,
converted_stat,
edited_stat,
info_json,
exif_json,
):
""" sets all the data for file and uuid at once
"""
filename = str(pathlib.Path(filename).relative_to(self._path))
@@ -329,6 +489,14 @@ class ExportDB(ExportDB_ABC):
+ "WHERE filepath_normalized = ?;",
(*exif_stat, filename_normalized),
)
c.execute(
"INSERT OR REPLACE INTO converted(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
(filename_normalized, *converted_stat),
)
c.execute(
"INSERT OR REPLACE INTO edited(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
(filename_normalized, *edited_stat),
)
c.execute(
"INSERT OR REPLACE INTO info(uuid, json_info) VALUES (?, ?);",
(uuid, info_json),
@@ -348,6 +516,37 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
def _set_stat_for_file(self, table, filename, stats):
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
conn = self._conn
c = conn.cursor()
c.execute(
f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
(filename, *stats),
)
conn.commit()
def _get_stat_for_file(self, table, filename):
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
c = conn.cursor()
c.execute(
f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
(filename,),
)
results = c.fetchone()
if results:
stats = results[0:3]
mtime = int(stats[2]) if stats[2] is not None else None
stats = (stats[0], stats[1], mtime)
else:
stats = (None, None, None)
return stats
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
@@ -355,16 +554,22 @@ class ExportDB(ExportDB_ABC):
"""
if not os.path.isfile(dbfile):
logging.debug(f"dbfile {dbfile} doesn't exist, creating it")
conn = self._get_db_connection(dbfile)
if conn:
self._create_db_tables(conn)
else:
if not conn:
raise Exception("Error getting connection to database {dbfile}")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else:
logging.debug(f"dbfile {dbfile} exists, opening it")
conn = self._get_db_connection(dbfile)
self.was_created = False
version_info = self._get_database_version(conn)
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
self._create_db_tables(conn)
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
def _get_db_connection(self, dbfile):
@@ -377,6 +582,13 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_database_version(self, conn):
""" return tuple of (osxphotos, exportdb) versions for database connection conn """
version_info = conn.execute(
"SELECT osxphotos, exportdb, max(id) FROM version"
).fetchone()
return (version_info[0], version_info[1])
def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
@@ -417,9 +629,34 @@ class ExportDB(ExportDB_ABC):
filepath_normalized TEXT NOT NULL,
json_exifdata JSON
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_table": """ CREATE TABLE IF NOT EXISTS edited (
id INTEGER PRIMARY KEY,
filepath_normalized TEXT NOT NULL,
mode INTEGER,
size INTEGER,
mtime REAL
); """,
"sql_converted_table": """ CREATE TABLE IF NOT EXISTS converted (
id INTEGER PRIMARY KEY,
filepath_normalized TEXT NOT NULL,
mode INTEGER,
size INTEGER,
mtime REAL
); """,
"sql_sidecar_table": """ CREATE TABLE IF NOT EXISTS sidecar (
id INTEGER PRIMARY KEY,
filepath_normalized TEXT NOT NULL,
sidecar_data TEXT,
mode INTEGER,
size INTEGER,
mtime REAL
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
}
try:
c = conn.cursor()
@@ -435,11 +672,10 @@ class ExportDB(ExportDB_ABC):
def __del__(self):
""" ensure the database connection is closed """
if self._conn:
try:
self._conn.close()
except Error as e:
logging.warning(e)
try:
self._conn.close()
except:
pass
def _insert_run_info(self):
dt = datetime.datetime.utcnow().isoformat()
@@ -478,18 +714,18 @@ class ExportDBInMemory(ExportDB):
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""
if not os.path.isfile(dbfile):
logging.debug(f"dbfile {dbfile} doesn't exist, creating in memory version")
conn = self._get_db_connection()
if conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
raise Exception("Error getting connection to in-memory database")
else:
logging.debug(f"dbfile {dbfile} exists, opening it and copying to memory")
try:
conn = sqlite3.connect(dbfile)
except Error as e:
@@ -506,6 +742,14 @@ class ExportDBInMemory(ExportDB):
conn = sqlite3.connect(":memory:")
conn.cursor().executescript(tempfile.read())
conn.commit()
self.was_created = False
_, exportdb_ver = self._get_database_version(conn)
if exportdb_ver < OSXPHOTOS_EXPORTDB_VERSION:
self._create_db_tables(conn)
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn

View File

@@ -1,6 +1,5 @@
""" FileUtil class with methods for copy, hardlink, unlink, etc. """
import logging
import os
import pathlib
import stat
@@ -8,6 +7,10 @@ import subprocess
import sys
from abc import ABC, abstractmethod
import CoreFoundation
from .imageconverter import ImageConverter
class FileUtilABC(ABC):
""" Abstract base class for FileUtil """
@@ -29,7 +32,22 @@ class FileUtilABC(ABC):
@classmethod
@abstractmethod
def cmp_sig(cls, file1, file2):
def rmdir(cls, dest):
pass
@classmethod
@abstractmethod
def utime(cls, path, times):
pass
@classmethod
@abstractmethod
def cmp(cls, file1, file2, mtime1=None):
pass
@classmethod
@abstractmethod
def cmp_file_sig(cls, file1, file2):
pass
@classmethod
@@ -37,6 +55,16 @@ class FileUtilABC(ABC):
def file_sig(cls, file1):
pass
@classmethod
@abstractmethod
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
pass
@classmethod
@abstractmethod
def rename(cls, src, dest):
pass
class FileUtilMacOS(FileUtilABC):
""" Various file utilities """
@@ -58,42 +86,41 @@ class FileUtilMacOS(FileUtilABC):
try:
os.link(src, dest)
except Exception as e:
logging.critical(f"os.link returned error: {e}")
raise e
@classmethod
def copy(cls, src, dest, norsrc=False):
def copy(cls, src, dest):
""" Copies a file from src path to dest path
src: source path as string
Args:
src: source path as string; must be a valid file path
dest: destination path as string
norsrc: (bool) if True, uses --norsrc flag with ditto so it will not copy
resource fork or extended attributes. May be useful on volumes that
don't work with extended attributes (likely only certain SMB mounts)
default is False
Uses ditto to perform copy; will silently overwrite dest if it exists
Raises exception if copy fails or either path is None """
dest may be either directory or file; in either case, src file must not exist in dest
Note: src and dest may be either a string or a pathlib.Path object
Returns:
True if copy succeeded
Raises:
OSError if copy fails
TypeError if either path is None
"""
if not isinstance(src, pathlib.Path):
src = pathlib.Path(src)
if src is None or dest is None:
raise ValueError("src and dest must not be None", src, dest)
if not isinstance(dest, pathlib.Path):
dest = pathlib.Path(dest)
if not os.path.isfile(src):
raise FileNotFoundError("src file does not appear to exist", src)
if dest.is_dir():
dest /= src.name
if norsrc:
command = ["/usr/bin/ditto", "--norsrc", src, dest]
else:
command = ["/usr/bin/ditto", src, dest]
# if error on copy, subprocess will raise CalledProcessError
try:
result = subprocess.run(command, check=True, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
logging.critical(
f"ditto returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
)
raise e
return result.returncode
filemgr = CoreFoundation.NSFileManager.defaultManager()
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
# error is a tuple of (bool, error_string)
# error[0] is True if copy succeeded
if not error[0]:
raise OSError(error[1])
return True
@classmethod
def unlink(cls, filepath):
@@ -104,11 +131,45 @@ class FileUtilMacOS(FileUtilABC):
os.unlink(filepath)
@classmethod
def cmp_sig(cls, f1, s2):
def rmdir(cls, dirpath):
""" remove directory filepath; dirpath must be empty """
if isinstance(dirpath, pathlib.Path):
dirpath.rmdir()
else:
os.rmdir(dirpath)
@classmethod
def utime(cls, path, times):
""" Set the access and modified time of path. """
os.utime(path, times)
@classmethod
def cmp(cls, f1, f2, mtime1=None):
"""Does shallow compare (file signatures) of f1 to file f2.
Arguments:
f1 -- File name
f2 -- File name
mtime1 -- optional, pass alternate file modification timestamp for f1; will be converted to int
Return value:
True if the file signatures as returned by stat are the same, False otherwise.
Does not do a byte-by-byte comparison.
"""
s1 = cls._sig(os.stat(f1))
if mtime1 is not None:
s1 = (s1[0], s1[1], int(mtime1))
s2 = cls._sig(os.stat(f2))
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
return False
return s1 == s2
@classmethod
def cmp_file_sig(cls, f1, s2):
"""Compare file f1 to signature s2.
Arguments:
f1 -- File name
s2 -- stats as returned by sig
s2 -- stats as returned by _sig
Return value:
True if the files are the same, False otherwise.
@@ -128,9 +189,46 @@ class FileUtilMacOS(FileUtilABC):
""" return os.stat signature for file f1 """
return cls._sig(os.stat(f1))
@classmethod
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
""" converts image file src_file to jpeg format as dest_file
Args:
src_file: image file to convert
dest_file: destination path to write converted file to
compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality)
Returns:
True if success, otherwise False
"""
converter = ImageConverter()
return converter.write_jpeg(
src_file, dest_file, compression_quality=compression_quality
)
@classmethod
def rename(cls, src, dest):
""" Copy src to dest
Args:
src: path to source file
dest: path to destination file
Returns:
Name of renamed file (dest)
"""
os.rename(str(src), str(dest))
return dest
@staticmethod
def _sig(st):
return (stat.S_IFMT(st.st_mode), st.st_size, st.st_mtime)
""" return tuple of (mode, size, mtime) of file based on os.stat
Args:
st: os.stat signature
"""
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
class FileUtil(FileUtilMacOS):
@@ -141,8 +239,8 @@ class FileUtil(FileUtilMacOS):
class FileUtilNoOp(FileUtil):
""" No-Op implementation of FileUtil for testing / dry-run mode
all methods with exception of cmp_sig and file_cmp are no-op
cmp_sig functions as FileUtil.cmp_sig does
all methods with exception of cmp, cmp_file_sig and file_cmp are no-op
cmp and cmp_file_sig functions as FileUtil methods do
file_cmp returns mock data
"""
@@ -172,7 +270,23 @@ class FileUtilNoOp(FileUtil):
def unlink(cls, dest):
cls.verbose(f"unlink: {dest}")
@classmethod
def rmdir(cls, dest):
cls.verbose(f"rmdir: {dest}")
@classmethod
def utime(cls, path, times):
cls.verbose(f"utime: {path}, {times}")
@classmethod
def file_sig(cls, file1):
cls.verbose(f"file_sig: {file1}")
return (42, 42, 42)
@classmethod
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
cls.verbose(f"convert_to_jpeg: {src_file}, {dest_file}, {compression_quality}")
@classmethod
def rename(cls, src, dest):
cls.verbose(f"rename: {src}, {dest}")

126
osxphotos/imageconverter.py Normal file
View File

@@ -0,0 +1,126 @@
""" ImageConverter class
Convert an image to JPEG using CoreImage --
for example, RAW to JPEG. Only works if Mac equipped with GPU. """
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
import pathlib
import objc
import Metal
import Quartz
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
class ImageConversionError(Exception):
"""Base class for exceptions in this module. """
pass
class ImageConverter:
""" Convert images to jpeg. This class is a singleton
which will re-use the Core Image CIContext to avoid
creating a new context for every conversion. """
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
return cls.instance
def __init__(self):
""" return existing singleton or create a new one """
if hasattr(self, "context"):
return
""" initialize CIContext """
context_options = NSDictionary.dictionaryWithDictionary_(
{
"workingColorSpace": Quartz.CoreGraphics.kCGColorSpaceExtendedSRGB,
"workingFormat": Quartz.kCIFormatRGBAh,
}
)
mtldevice = Metal.MTLCreateSystemDefaultDevice()
self.context = Quartz.CIContext.contextWithMTLDevice_options_(
mtldevice, context_options
)
def write_jpeg(self, input_path, output_path, compression_quality=1.0):
""" convert image to jpeg and write image to output_path
Args:
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
output_path: path to exported jpeg (e.g. '/path/to/export/file.jpeg') as str or pathlib.Path
compression_quality: JPEG compression quality, float in range 0.0 to 1.0; default is 1.0 (best quality)
Return:
True if conversion successful, else False
Raises:
ValueError if compression quality not in range 0.0 to 1.0
FileNotFoundError if input_path doesn't exist
ImageConversionError if error during conversion
"""
# Set up a dedicated objc autorelease pool for this function call.
# This is to ensure that all the NSObjects are cleaned up after each
# call to prevent memory leaks.
# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
# https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management
with objc.autorelease_pool():
# accept input_path or output_path as pathlib.Path
if not isinstance(input_path, str):
input_path = str(input_path)
if not isinstance(output_path, str):
output_path = str(output_path)
if not pathlib.Path(input_path).is_file():
raise FileNotFoundError(f"could not find {input_path}")
if not (0.0 <= compression_quality <= 1.0):
raise ValueError(
"illegal value for compression_quality: {compression_quality}"
)
input_url = NSURL.fileURLWithPath_(input_path)
output_url = NSURL.fileURLWithPath_(output_path)
with pipes() as (out, err):
# capture stdout and stderr from system calls
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
# prints to stderr something like:
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
if input_image is None:
raise ImageConversionError(f"Could not create CIImage for {input_path}")
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
Quartz.CoreGraphics.kCGColorSpaceSRGB
)
output_options = NSDictionary.dictionaryWithDictionary_(
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
)
(
_,
error,
) = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
input_image, output_url, output_colorspace, output_options, None
)
if not error:
return True
else:
raise ImageConversionError(
"Error converting file {input_path} to jpeg at {output_path}: {error}"
)

79
osxphotos/path_utils.py Normal file
View File

@@ -0,0 +1,79 @@
""" utility functions for validating/sanitizing path components """
import pathvalidate
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
def sanitize_filepath(filepath):
""" sanitize a filepath """
return pathvalidate.sanitize_filepath(filepath, platform="macos")
def is_valid_filepath(filepath):
""" returns True if a filepath is valid otherwise False """
return pathvalidate.is_valid_filepath(filepath, platform="macos")
def sanitize_filename(filename, replacement=":"):
""" replace any illegal characters in a filename and truncate filename if needed
Args:
filename: str, filename to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
Returns:
filename with any illegal characters replaced by replacement and truncated if necessary
"""
if filename:
filename = filename.replace("/", replacement)
if len(filename) > MAX_FILENAME_LEN:
parts = filename.split(".")
drop = len(filename) - MAX_FILENAME_LEN
if len(parts) > 1:
# has an extension
ext = parts.pop(-1)
stem = ".".join(parts)
if drop > len(stem):
ext = ext[:-drop]
else:
stem = stem[:-drop]
filename = f"{stem}.{ext}"
else:
filename = filename[:-drop]
return filename
def sanitize_dirname(dirname, replacement=":"):
""" replace any illegal characters in a directory name and truncate directory name if needed
Args:
dirname: str, directory name to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
Returns:
dirname with any illegal characters replaced by replacement and truncated if necessary
"""
if dirname:
dirname = sanitize_pathpart(dirname, replacement=replacement)
return dirname
def sanitize_pathpart(pathpart, replacement=":"):
""" replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
Args:
pathpart: str, path part to sanitze
replacement: str, value to replace any illegal characters with; default = ":"
Returns:
pathpart with any illegal characters replaced by replacement and truncated if necessary
"""
if pathpart:
pathpart = pathpart.replace("/", replacement)
if len(pathpart) > MAX_DIRNAME_LEN:
drop = len(pathpart) - MAX_DIRNAME_LEN
pathpart = pathpart[:-drop]
return pathpart

501
osxphotos/personinfo.py Normal file
View File

@@ -0,0 +1,501 @@
""" PhotoInfo and FaceInfo classes to expose info about persons and faces in the Photos library """
import json
import logging
import math
from collections import namedtuple
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
class PersonInfo:
"""Info about a person in the Photos library"""
def __init__(self, db=None, pk=None):
"""Creates a new PersonInfo instance
Arguments:
db: instance of PhotosDB object
pk: primary key value of person to initialize PersonInfo with
Returns:
PersonInfo instance
"""
self._db = db
self._pk = pk
person = self._db._dbpersons_pk[pk]
self.uuid = person["uuid"]
self.name = person["fullname"]
self.display_name = person["displayname"]
self.keyface = person["keyface"]
self.facecount = person["facecount"]
@property
def keyphoto(self):
try:
return self._keyphoto
except AttributeError:
person = self._db._dbpersons_pk[self._pk]
if person["photo_uuid"]:
try:
key_photo = self._db.get_photo(person["photo_uuid"])
except IndexError:
key_photo = None
else:
key_photo = None
self._keyphoto = key_photo
return self._keyphoto
@property
def photos(self):
""" Returns list of PhotoInfo objects associated with this person """
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
@property
def face_info(self):
"""Returns a list of FaceInfo objects associated with this person sorted by quality score
Highest quality face is result[0] and lowest quality face is result[n]
"""
try:
faces = self._db._db_faceinfo_person[self._pk]
return sorted(
[FaceInfo(db=self._db, pk=face) for face in faces],
key=lambda face: face.quality,
reverse=True,
)
except KeyError:
# no faces
return []
def asdict(self):
""" Returns dictionary representation of class instance """
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
return {
"uuid": self.uuid,
"name": self.name,
"displayname": self.display_name,
"keyface": self.keyface,
"facecount": self.facecount,
"keyphoto": keyphoto,
}
def json(self):
""" Returns JSON representation of class instance """
return json.dumps(self.asdict())
def __str__(self):
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return all(
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
)
def __ne__(self, other):
return not self.__eq__(other)
class FaceInfo:
"""Info about a face in the Photos library"""
def __init__(self, db=None, pk=None):
"""Creates a new FaceInfo instance
Arguments:
db: instance of PhotosDB object
pk: primary key value of face to init the object with
Returns:
FaceInfo instance
"""
self._db = db
self._pk = pk
face = self._db._db_faceinfo_pk[pk]
self._info = face
self.uuid = face["uuid"]
self.name = face["fullname"]
self.asset_uuid = face["asset_uuid"]
self._person_pk = face["person"]
self.center_x = face["centerx"]
self.center_y = face["centery"]
self.mouth_x = face["mouthx"]
self.mouth_y = face["mouthy"]
self.left_eye_x = face["lefteyex"]
self.left_eye_y = face["lefteyey"]
self.right_eye_x = face["righteyex"]
self.right_eye_y = face["righteyey"]
self.size = face["size"]
self.quality = face["quality"]
self.source_width = face["sourcewidth"]
self.source_height = face["sourceheight"]
self.has_smile = face["has_smile"]
self.left_eye_closed = face["left_eye_closed"]
self.right_eye_closed = face["right_eye_closed"]
self.manual = face["manual"]
self.face_type = face["facetype"]
self.age_type = face["agetype"]
self.bald_type = face["baldtype"]
self.eye_makeup_type = face["eyemakeuptype"]
self.eye_state = face["eyestate"]
self.facial_hair_type = face["facialhairtype"]
self.gender_type = face["gendertype"]
self.glasses_type = face["glassestype"]
self.hair_color_type = face["haircolortype"]
self.intrash = face["intrash"]
self.lip_makeup_type = face["lipmakeuptype"]
self.smile_type = face["smiletype"]
@property
def center(self):
"""Coordinates, in PIL format, for center of face
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point((self.center_x, self.center_y))
@property
def size_pixels(self):
"""Size of face in pixels (centered around center_x, center_y)
Returns:
size, in int pixels, of a circle drawn around the center of the face
"""
photo = self.photo
size_reference = photo.width if photo.width > photo.height else photo.height
return self.size * size_reference
@property
def mouth(self):
"""Coordinates, in PIL format, for mouth position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.mouth_x, self.mouth_y))
@property
def left_eye(self):
"""Coordinates, in PIL format, for left eye position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.left_eye_x, self.left_eye_y))
@property
def right_eye(self):
"""Coordinates, in PIL format, for right eye position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.right_eye_x, self.right_eye_y))
@property
def person_info(self):
""" PersonInfo instance for person associated with this face """
try:
return self._person
except AttributeError:
self._person = PersonInfo(db=self._db, pk=self._person_pk)
return self._person
@property
def photo(self):
""" PhotoInfo instance associated with this face """
try:
return self._photo
except AttributeError:
self._photo = self._db.get_photo(self.asset_uuid)
if self._photo is None:
logging.warning(f"Could not get photo for uuid: {self.asset_uuid}")
return self._photo
@property
def mwg_rs_area(self):
"""Get coordinates for Metadata Working Group Region Area.
Returns:
MWG_RS_Area named tuple with x, y, h, w where:
x = stArea:x
y = stArea:y
h = stArea:h
w = stArea:w
Reference:
https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region
"""
x, y = self.center_x, self.center_y
x, y = self._fix_orientation((x, y))
if self.photo.orientation in [5, 6, 7, 8]:
w = self.size_pixels / self.photo.height
h = self.size_pixels / self.photo.width
else:
h = self.size_pixels / self.photo.height
w = self.size_pixels / self.photo.width
return MWG_RS_Area(x, y, h, w)
@property
def mpri_reg_rect(self):
"""Get coordinates for Microsoft Photo Region Rectangle.
Returns:
MPRI_Reg_Rect named tuple with x, y, h, w where:
x = x coordinate of top left corner of rectangle
y = y coordinate of top left corner of rectangle
h = height of rectangle
w = width of rectangle
Reference:
https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging
"""
x, y = self.center_x, self.center_y
x, y = self._fix_orientation((x, y))
if self.photo.orientation in [5, 6, 7, 8]:
w = self.size_pixels / self.photo.width
h = self.size_pixels / self.photo.height
x = x - self.size_pixels / self.photo.height / 2
y = y - self.size_pixels / self.photo.width / 2
else:
h = self.size_pixels / self.photo.width
w = self.size_pixels / self.photo.height
x = x - self.size_pixels / self.photo.width / 2
y = y - self.size_pixels / self.photo.height / 2
return MPRI_Reg_Rect(x, y, h, w)
def face_rect(self):
"""Get face rectangle coordinates for current version of the associated image
If image has been edited, rectangle applies to edited version, otherwise original version
Coordinates in format and reference frame used by PIL
Returns:
list [(x0, x1), (y0, y1)] of coordinates in reference frame used by PIL
"""
photo = self.photo
size_reference = photo.width if photo.width > photo.height else photo.height
radius = (self.size / 2) * size_reference
x, y = self._make_point((self.center_x, self.center_y))
x0, y0 = x - radius, y - radius
x1, y1 = x + radius, y + radius
return [(x0, y0), (x1, y1)]
def roll_pitch_yaw(self):
""" Roll, pitch, yaw of face in radians as tuple """
info = self._info
roll = 0 if info["roll"] is None else info["roll"]
pitch = 0 if info["pitch"] is None else info["pitch"]
yaw = 0 if info["yaw"] is None else info["yaw"]
return (roll, pitch, yaw)
@property
def roll(self):
""" Return roll angle in radians of the face region """
roll, _, _ = self.roll_pitch_yaw()
return roll
@property
def pitch(self):
""" Return pitch angle in radians of the face region """
_, pitch, _ = self.roll_pitch_yaw()
return pitch
@property
def yaw(self):
""" Return yaw angle in radians of the face region """
_, _, yaw = self.roll_pitch_yaw()
return yaw
def _fix_orientation(self, xy):
"""Translate an (x, y) tuple based on image orientation
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates
"""
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
orientation = self.photo.orientation
x, y = xy
if orientation == 1:
y = 1.0 - y
elif orientation == 2:
y = 1.0 - y
x = 1.0 - x
elif orientation == 3:
x = 1.0 - x
elif orientation == 4:
pass
elif orientation == 5:
x, y = 1.0 - y, x
elif orientation == 6:
x, y = 1.0 - y, 1.0 - x
elif orientation == 7:
x, y = y, x
y = 1.0 - y
elif orientation == 8:
x, y = y, x
elif orientation == 0:
# set by osxphotos if adjusted orientation cannot be read, assume it's 1
y = 1.0 - y
else:
logging.warning(f"Unhandled orientation: {orientation}")
return (x, y)
def _make_point(self, xy):
"""Translate an (x, y) tuple based on image orientation
and convert to image coordinates
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame
"""
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
orientation = self.photo.orientation
x, y = self._fix_orientation(xy)
dx = self.photo.width
dy = self.photo.height
if orientation in [5, 6, 7, 8]:
dx, dy = dy, dx
return (int(x * dx), int(y * dy))
def _make_point_with_rotation(self, xy):
"""Translate an (x, y) tuple based on image orientation and rotation
and convert to image coordinates
Arguments:
xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width)
Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame
"""
# convert to image coordinates
x, y = self._make_point(xy)
# rotate about center
xmid, ymid = self.center
roll, _, _ = self.roll_pitch_yaw()
xr, yr = rotate_image_point(x, y, xmid, ymid, roll)
return (int(xr), int(yr))
def asdict(self):
""" Returns dict representation of class instance """
roll, pitch, yaw = self.roll_pitch_yaw()
return {
"_pk": self._pk,
"uuid": self.uuid,
"name": self.name,
"asset_uuid": self.asset_uuid,
"_person_pk": self._person_pk,
"center_x": self.center_x,
"center_y": self.center_y,
"center": self.center,
"mouth_x": self.mouth_x,
"mouth_y": self.mouth_y,
"mouth": self.mouth,
"left_eye_x": self.left_eye_x,
"left_eye_y": self.left_eye_y,
"left_eye": self.left_eye,
"right_eye_x": self.right_eye_x,
"right_eye_y": self.right_eye_y,
"right_eye": self.right_eye,
"size": self.size,
"face_rect": self.face_rect(),
"mpri_reg_rect": self.mpri_reg_rect._asdict(),
"mwg_rs_area": self.mwg_rs_area._asdict(),
"roll": roll,
"pitch": pitch,
"yaw": yaw,
"quality": self.quality,
"source_width": self.source_width,
"source_height": self.source_height,
"has_smile": self.has_smile,
"left_eye_closed": self.left_eye_closed,
"right_eye_closed": self.right_eye_closed,
"manual": self.manual,
"face_type": self.face_type,
"age_type": self.age_type,
"bald_type": self.bald_type,
"eye_makeup_type": self.eye_makeup_type,
"eye_state": self.eye_state,
"facial_hair_type": self.facial_hair_type,
"gender_type": self.gender_type,
"glasses_type": self.glasses_type,
"hair_color_type": self.hair_color_type,
"intrash": self.intrash,
"lip_makeup_type": self.lip_makeup_type,
"smile_type": self.smile_type,
}
def json(self):
""" Return JSON representation of FaceInfo instance """
return json.dumps(self.asdict())
def __str__(self):
return f"FaceInfo(uuid={self.uuid}, center_x={self.center_x}, center_y = {self.center_y}, size={self.size}, person={self.name}, asset_uuid={self.asset_uuid})"
def __repr__(self):
return f"FaceInfo(db={self._db}, pk={self._pk})"
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return all(
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
)
def __ne__(self, other):
return not self.__eq__(other)
def rotate_image_point(x, y, xmid, ymid, angle):
"""rotate image point about xm, ym by angle in radians
Arguments:
x: x coordinate of point to rotate
y: y coordinate of point to rotate
xmid: x coordinate of center point to rotate about
ymid: y coordinate of center point to rotate about
angle: angle in radians about which to coordinate,
counter-clockwise is positive
Returns:
tuple of rotated points (xr, yr)
"""
# translate point relative to the mid point
x = x - xmid
y = y - ymid
# rotate by angle and translate back
# the photo coordinate system is downwards y is positive so
# need to adjust the rotation accordingly
cos_angle = math.cos(angle)
sin_angle = math.sin(angle)
xr = x * cos_angle + y * sin_angle + xmid
yr = -x * sin_angle + y * cos_angle + ymid
return (xr, yr)

View File

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

View File

@@ -5,6 +5,7 @@ import os
from ..exiftool import ExifTool, get_exiftool_path
@property
def exiftool(self):
""" Returns an ExifTool object for the photo
@@ -17,17 +18,17 @@ def exiftool(self):
return self._exiftool
except AttributeError:
try:
exiftool_path = get_exiftool_path()
exiftool_path = self._db._exiftool_path or get_exiftool_path()
if self.path is not None and os.path.isfile(self.path):
exiftool = ExifTool(self.path)
exiftool = ExifTool(self.path, exiftool=exiftool_path)
else:
exiftool = None
logging.debug(f"exiftool: missing path {self.uuid}")
except FileNotFoundError:
# get_exiftool_path raises FileNotFoundError if exiftool not found
exiftool = None
logging.warning(f"exiftool not in path; download and install from https://exiftool.org/")
logging.warning(
f"exiftool not in path; download and install from https://exiftool.org/"
)
self._exiftool = exiftool
return self._exiftool

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,32 @@
""" Methods and class for PhotoInfo exposing SearchInfo data such as labels
Adds the following properties to PhotoInfo (valid only for Photos 5):
search_info: returns a SearchInfo object
search_info_normalized: returns a SearchInfo object with properties that produce normalized results
labels: returns list of labels
labels_normalized: returns list of normalized labels
"""
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL
from .._constants import (
_PHOTOS_4_VERSION,
SEARCH_CATEGORY_CITY,
SEARCH_CATEGORY_LABEL,
SEARCH_CATEGORY_NEIGHBORHOOD,
SEARCH_CATEGORY_PLACE_NAME,
SEARCH_CATEGORY_STREET,
SEARCH_CATEGORY_ALL_LOCALITY,
SEARCH_CATEGORY_COUNTRY,
SEARCH_CATEGORY_STATE,
SEARCH_CATEGORY_STATE_ABBREVIATION,
SEARCH_CATEGORY_BODY_OF_WATER,
SEARCH_CATEGORY_MONTH,
SEARCH_CATEGORY_YEAR,
SEARCH_CATEGORY_HOLIDAY,
SEARCH_CATEGORY_ACTIVITY,
SEARCH_CATEGORY_SEASON,
SEARCH_CATEGORY_VENUE,
SEARCH_CATEGORY_VENUE_TYPE,
SEARCH_CATEGORY_MEDIA_TYPES,
)
@property
@@ -24,6 +45,22 @@ def search_info(self):
return self._search_info
@property
def search_info_normalized(self):
""" returns SearchInfo object for photo that produces normalized results
only valid on Photos 5, on older libraries, returns None
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
# memoize SearchInfo object
try:
return self._search_info_normalized
except AttributeError:
self._search_info_normalized = SearchInfo(self, normalized=True)
return self._search_info_normalized
@property
def labels(self):
""" returns list of labels applied to photo by Photos image categorization
@@ -43,14 +80,15 @@ def labels_normalized(self):
if self._db._db_version <= _PHOTOS_4_VERSION:
return []
return self.search_info.labels_normalized
return self.search_info_normalized.labels
class SearchInfo:
""" Info about search terms such as machine learning labels that Photos knows about a photo """
def __init__(self, photo):
""" photo: PhotoInfo object """
def __init__(self, photo, normalized=False):
""" photo: PhotoInfo object
normalized: if True, all properties return normalized (lower case) results """
if photo._db._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
@@ -58,6 +96,7 @@ class SearchInfo:
)
self._photo = photo
self._normalized = normalized
self.uuid = photo.uuid
try:
# get search info for this UUID
@@ -69,25 +108,170 @@ class SearchInfo:
@property
def labels(self):
""" return list of labels associated with Photo """
if self._db_searchinfo:
labels = [
rec["content_string"]
for rec in self._db_searchinfo
if rec["category"] == SEARCH_CATEGORY_LABEL
]
else:
labels = []
return labels
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
@property
def labels_normalized(self):
""" return list of normalized labels associated with Photo """
def place_names(self):
""" returns list of place names """
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
@property
def streets(self):
""" returns list of street names """
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
@property
def neighborhoods(self):
""" returns list of neighborhoods """
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
@property
def locality_names(self):
""" returns list of other locality names """
locality = []
for category in SEARCH_CATEGORY_ALL_LOCALITY:
locality += self._get_text_for_category(category)
return locality
@property
def city(self):
""" returns city/town """
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
return city[0] if city else ""
@property
def state(self):
""" returns state name """
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
return state[0] if state else ""
@property
def state_abbreviation(self):
""" returns state abbreviation """
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
return abbrev[0] if abbrev else ""
@property
def country(self):
""" returns country name """
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
return country[0] if country else ""
@property
def month(self):
""" returns month name """
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
return month[0] if month else ""
@property
def year(self):
""" returns year """
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
return year[0] if year else ""
@property
def bodies_of_water(self):
""" returns list of body of water names """
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
@property
def holidays(self):
""" returns list of holiday names """
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
@property
def activities(self):
""" returns list of activity names """
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
@property
def season(self):
""" returns season name """
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
return season[0] if season else ""
@property
def venues(self):
""" returns list of venue names """
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
@property
def venue_types(self):
""" returns list of venue types """
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
@property
def media_types(self):
""" returns list of media types (photo, video, panorama, etc) """
types = []
for category in SEARCH_CATEGORY_MEDIA_TYPES:
types += self._get_text_for_category(category)
return types
@property
def all(self):
""" return all search info properties in a single list """
all = (
self.labels
+ self.place_names
+ self.streets
+ self.neighborhoods
+ self.locality_names
+ self.bodies_of_water
+ self.holidays
+ self.activities
+ self.venues
+ self.venue_types
+ self.media_types
)
if self.city:
all += [self.city]
if self.state:
all += [self.state]
if self.state_abbreviation:
all += [self.state_abbreviation]
if self.country:
all += [self.country]
if self.month:
all += [self.month]
if self.year:
all += [self.year]
if self.season:
all += [self.season]
return all
def asdict(self):
""" return dict of search info """
return {
"labels": self.labels,
"place_names": self.place_names,
"streets": self.streets,
"neighborhoods": self.neighborhoods,
"city": self.city,
"locality_names": self.locality_names,
"state": self.state,
"state_abbreviation": self.state_abbreviation,
"country": self.country,
"bodies_of_water": self.bodies_of_water,
"month": self.month,
"year": self.year,
"holidays": self.holidays,
"activities": self.activities,
"season": self.season,
"venues": self.venues,
"venue_types": self.venue_types,
"media_types": self.media_types,
}
def _get_text_for_category(self, category):
""" return list of text for a specified category ID """
if self._db_searchinfo:
labels = [
rec["normalized_string"]
content = "normalized_string" if self._normalized else "content_string"
return [
rec[content]
for rec in self._db_searchinfo
if rec["category"] == SEARCH_CATEGORY_LABEL
if rec["category"] == category
]
else:
labels = []
return labels
return []

View File

@@ -5,16 +5,13 @@ PhotosDB.photos() returns a list of PhotoInfo objects
"""
import dataclasses
import glob
import datetime
import json
import logging
import os
import os.path
import pathlib
import subprocess
import sys
from datetime import timedelta, timezone
from pprint import pformat
import yaml
@@ -25,10 +22,14 @@ from .._constants import (
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
_PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION,
)
from ..albuminfo import AlbumInfo
from ..adjustmentsinfo import AdjustmentsInfo
from ..albuminfo import AlbumInfo, ImportInfo
from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
@@ -43,6 +44,7 @@ class PhotoInfo:
# import additional methods
from ._photoinfo_searchinfo import (
search_info,
search_info_normalized,
labels,
labels_normalized,
SearchInfo,
@@ -53,49 +55,69 @@ class PhotoInfo:
export,
export2,
_export_photo,
_exiftool_dict,
_exiftool_json_sidecar,
_get_exif_keywords,
_get_exif_persons,
_write_exif_data,
_write_sidecar,
_xmp_sidecar,
ExportResults,
)
from ._photoinfo_scoreinfo import score, ScoreInfo
from ._photoinfo_comments import comments, likes
def __init__(self, db=None, uuid=None, info=None):
self._uuid = uuid
self._info = info
self._db = db
self._verbose = self._db._verbose
@property
def filename(self):
""" filename of the picture """
# sourcery off
if self.has_raw and self.raw_original:
# return name of the RAW file
# TODO: not yet implemented
return self._info["filename"]
if (
self._db._db_version <= _PHOTOS_4_VERSION
and self.has_raw
and self.raw_original
):
# return the JPEG version as that's what Photos 5+ does
return self._info["raw_pair_info"]["filename"]
else:
return self._info["filename"]
@property
def original_filename(self):
""" original filename of the picture
Photos 5 mangles filenames upon import """
return self._info["originalFilename"]
"""original filename of the picture
Photos 5 mangles filenames upon import"""
if (
self._db._db_version <= _PHOTOS_4_VERSION
and self.has_raw
and self.raw_original
):
# return the JPEG version as that's what Photos 5+ does
original_name = self._info["raw_pair_info"]["originalFilename"]
else:
original_name = self._info["originalFilename"]
return original_name or self.filename
@property
def date(self):
""" image creation date as timezone aware datetime object """
imagedate = self._info["imageDate"]
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
delta = timedelta(seconds=seconds)
tz = timezone(delta)
return imagedate.astimezone(tz=tz)
return self._info["imageDate"]
@property
def date_modified(self):
""" image modification date as timezone aware datetime object
or None if no modification date set """
"""image modification date as timezone aware datetime object
or None if no modification date set"""
# Photos <= 4 provides no way to get date of adjustment and will update
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
# only report lastmodified date for Photos <=4 if photo is edited;
# even in this case, the date could be incorrect
if not self.hasadjustments and self._db._db_version <= _PHOTOS_4_VERSION:
return None
imagedate = self._info["lastmodifieddate"]
if imagedate:
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
@@ -113,151 +135,204 @@ class PhotoInfo:
@property
def path(self):
""" absolute path on disk of the original picture """
try:
return self._path
except AttributeError:
self._path = None
photopath = None
# TODO: should path try to return path even if ismissing?
if self._info["isMissing"] == 1:
return photopath # path would be meaningless until downloaded
photopath = None
if self._info["isMissing"] == 1:
return photopath # path would be meaningless until downloaded
if self._db._db_version <= _PHOTOS_4_VERSION:
if self._info["has_raw"]:
# return the path to JPEG even if RAW is original
vol = (
self._db._dbvolumes[self._info["raw_pair_info"]["volumeId"]]
if self._info["raw_pair_info"]["volumeId"] is not None
else None
)
if vol is not None:
photopath = os.path.join(
"/Volumes", vol, self._info["raw_pair_info"]["imagePath"]
)
else:
photopath = os.path.join(
self._db._masters_path,
self._info["raw_pair_info"]["imagePath"],
)
else:
vol = self._info["volume"]
if vol is not None:
photopath = os.path.join(
"/Volumes", vol, self._info["imagePath"]
)
else:
photopath = os.path.join(
self._db._masters_path, self._info["imagePath"]
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
if self._db._db_version <= _PHOTOS_4_VERSION:
vol = self._info["volume"]
if vol is not None:
photopath = os.path.join("/Volumes", vol, self._info["imagePath"])
if self._info["shared"]:
# shared photo
photopath = os.path.join(
self._db._library_path,
_PHOTOS_5_SHARED_PHOTO_PATH,
self._info["directory"],
self._info["filename"],
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
if self._info["directory"].startswith("/"):
photopath = os.path.join(
self._info["directory"], self._info["filename"]
)
else:
photopath = os.path.join(
self._db._masters_path, self._info["imagePath"]
self._db._masters_path,
self._info["directory"],
self._info["filename"],
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
# TODO: Is there a way to use applescript or PhotoKit to force the download in this
if self._info["shared"]:
# shared photo
photopath = os.path.join(
self._db._library_path,
_PHOTOS_5_SHARED_PHOTO_PATH,
self._info["directory"],
self._info["filename"],
)
return photopath
if self._info["directory"].startswith("/"):
photopath = os.path.join(self._info["directory"], self._info["filename"])
else:
photopath = os.path.join(
self._db._masters_path, self._info["directory"], self._info["filename"]
)
return photopath
@property
def path_edited(self):
""" absolute path on disk of the edited picture """
""" None if photo has not been edited """
# TODO: break this code into a _path_edited_4 and _path_edited_5
# version to simplify the big if/then; same for path_live_photo
try:
return self._path_edited
except AttributeError:
if self._db._db_version <= _PHOTOS_4_VERSION:
self._path_edited = self._path_edited_4()
else:
self._path_edited = self._path_edited_5()
return self._path_edited
def _path_edited_5(self):
""" return path_edited for Photos >= 5 """
# In Photos 5.0 / Catalina / MacOS 10.15:
# edited photos appear to always be converted to .jpeg and stored in
# library_name/resources/renders/X/UUID_1_201_a.jpeg
# where X = first letter of UUID
# and UUID = UUID of image
# this seems to be true even for photos not copied to Photos library and
# where original format was not jpg/jpeg
# if more than one edit, previous edit is stored as UUID_p.jpeg
#
# In Photos 6.0 / Big Sur, the edited image is a .heic if the photo isn't a jpeg,
# otherwise it's a jpeg. It could also be a jpeg if photo library upgraded from earlier
# version.
if self._db._db_version < _PHOTOS_5_VERSION:
raise RuntimeError("Wrong database format!")
if self._info["hasAdjustments"]:
library = self._db._library_path
directory = self._uuid[0] # first char of uuid
filename = None
if self._info["type"] == _PHOTO_TYPE:
# it's a photo
if self._db._photos_ver == 5:
filename = f"{self._uuid}_1_201_a.jpeg"
else:
# could be a heic or a jpeg
if self.uti == "public.heic":
filename = f"{self._uuid}_1_201_a.heic"
else:
filename = f"{self._uuid}_1_201_a.jpeg"
elif self._info["type"] == _MOVIE_TYPE:
# it's a movie
filename = f"{self._uuid}_2_0_a.mov"
else:
# don't know what it is!
logging.debug(f"WARNING: unknown type {self._info['type']}")
return None
photopath = os.path.join(
library, "resources", "renders", directory, filename
)
if not os.path.isfile(photopath):
logging.debug(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
photopath = None
# TODO: might be possible for original/master to be missing but edit to still be there
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
return photopath
def _path_edited_4(self):
""" return path_edited for Photos <= 4 """
if self._db._db_version > _PHOTOS_4_VERSION:
raise RuntimeError("Wrong database format!")
photopath = None
if self._db._db_version <= _PHOTOS_4_VERSION:
if self._info["hasAdjustments"]:
edit_id = self._info["edit_resource_id"]
if edit_id is not None:
library = self._db._library_path
folder_id, file_id = _get_resource_loc(edit_id)
# todo: is this always true or do we need to search file file_id under folder_id
# figure out what kind it is and build filename
filename = None
if self._info["type"] == _PHOTO_TYPE:
# it's a photo
filename = f"fullsizeoutput_{file_id}.jpeg"
elif self._info["type"] == _MOVIE_TYPE:
# it's a movie
filename = f"fullsizeoutput_{file_id}.mov"
else:
# don't know what it is!
logging.debug(f"WARNING: unknown type {self._info['type']}")
return None
# photopath appears to usually be in "00" subfolder but
# could be elsewhere--I haven't figured out this logic yet
# first see if it's in 00
photopath = os.path.join(
library,
"resources",
"media",
"version",
folder_id,
"00",
filename,
)
if not os.path.isfile(photopath):
rootdir = os.path.join(
library, "resources", "media", "version", folder_id
)
for dirname, _, filelist in os.walk(rootdir):
if filename in filelist:
photopath = os.path.join(dirname, filename)
break
# check again to see if we found a valid file
if not os.path.isfile(photopath):
logging.debug(
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
logging.debug(
f"{self.uuid} hasAdjustments but edit_resource_id is None"
)
photopath = None
else:
photopath = None
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
else:
# in Photos 5.0 / Catalina / MacOS 10.15:
# edited photos appear to always be converted to .jpeg and stored in
# library_name/resources/renders/X/UUID_1_201_a.jpeg
# where X = first letter of UUID
# and UUID = UUID of image
# this seems to be true even for photos not copied to Photos library and
# where original format was not jpg/jpeg
# if more than one edit, previous edit is stored as UUID_p.jpeg
if self._info["hasAdjustments"]:
if self._info["hasAdjustments"]:
edit_id = self._info["edit_resource_id"]
if edit_id is not None:
library = self._db._library_path
directory = self._uuid[0] # first char of uuid
folder_id, file_id = _get_resource_loc(edit_id)
# todo: is this always true or do we need to search file file_id under folder_id
# figure out what kind it is and build filename
filename = None
if self._info["type"] == _PHOTO_TYPE:
# it's a photo
filename = f"{self._uuid}_1_201_a.jpeg"
filename = f"fullsizeoutput_{file_id}.jpeg"
elif self._info["type"] == _MOVIE_TYPE:
# it's a movie
filename = f"{self._uuid}_2_0_a.mov"
filename = f"fullsizeoutput_{file_id}.mov"
else:
# don't know what it is!
logging.debug(f"WARNING: unknown type {self._info['type']}")
return None
# photopath appears to usually be in "00" subfolder but
# could be elsewhere--I haven't figured out this logic yet
# first see if it's in 00
photopath = os.path.join(
library, "resources", "renders", directory, filename
library, "resources", "media", "version", folder_id, "00", filename
)
if not os.path.isfile(photopath):
rootdir = os.path.join(
library, "resources", "media", "version", folder_id
)
for dirname, _, filelist in os.walk(rootdir):
if filename in filelist:
photopath = os.path.join(dirname, filename)
break
# check again to see if we found a valid file
if not os.path.isfile(photopath):
logging.debug(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
logging.debug(
f"{self.uuid} hasAdjustments but edit_resource_id is None"
)
photopath = None
# TODO: might be possible for original/master to be missing but edit to still be there
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
# logging.debug(photopath)
else:
photopath = None
return photopath
@@ -339,7 +414,32 @@ class PhotoInfo:
@property
def persons(self):
""" list of persons in picture """
return self._info["persons"]
return [self._db._dbpersons_pk[pk]["fullname"] for pk in self._info["persons"]]
@property
def person_info(self):
""" list of PersonInfo objects for person in picture """
try:
return self._personinfo
except AttributeError:
self._personinfo = [
PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]
]
return self._personinfo
@property
def face_info(self):
""" list of FaceInfo objects for faces in picture """
try:
return self._faceinfo
except AttributeError:
try:
faces = self._db._db_faceinfo_uuid[self._uuid]
self._faceinfo = [FaceInfo(db=self._db, pk=pk) for pk in faces]
except KeyError:
# no faces
self._faceinfo = []
return self._faceinfo
@property
def albums(self):
@@ -365,6 +465,19 @@ class PhotoInfo:
]
return self._album_info
@property
def import_info(self):
""" ImportInfo object representing import session for the photo or None if no import session """
try:
return self._import_info
except AttributeError:
self._import_info = (
ImportInfo(db=self._db, uuid=self._info["import_uuid"])
if self._info["import_uuid"] is not None
else None
)
return self._import_info
@property
def keywords(self):
""" list of keywords for picture """
@@ -382,46 +495,85 @@ class PhotoInfo:
@property
def ismissing(self):
""" returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
"""returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
do not immediately get written to disk. In particular, I've noticed that downloading
do not immediately get written to disk. In particular, I've noticed that downloading
an image from the cloud does not force the database to be updated until something else
e.g. an edit, keyword, etc. occurs forcing a database synch
The exact process / timing is a mystery to be but be aware that if some photos were recently
downloaded from cloud to local storate their status in the database might still show
isMissing = 1
"""
return True if self._info["isMissing"] == 1 else False
return self._info["isMissing"] == 1
@property
def hasadjustments(self):
""" True if picture has adjustments / edits """
return True if self._info["hasAdjustments"] == 1 else False
return self._info["hasAdjustments"] == 1
@property
def adjustments(self):
""" Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only """
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
if self.hasadjustments:
try:
return self._adjustmentinfo
except AttributeError:
library = self._db._library_path
directory = self._uuid[0] # first char of uuid
plist_file = (
pathlib.Path(library)
/ "resources"
/ "renders"
/ directory
/ f"{self._uuid}.plist"
)
if not plist_file.is_file():
return None
self._adjustmentinfo = AdjustmentsInfo(plist_file)
return self._adjustmentinfo
@property
def external_edit(self):
""" Returns True if picture was edited outside of Photos using external editor """
return (
True
if self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
else False
)
return self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
@property
def favorite(self):
""" True if picture is marked as favorite """
return True if self._info["favorite"] == 1 else False
return self._info["favorite"] == 1
@property
def hidden(self):
""" True if picture is hidden """
return True if self._info["hidden"] == 1 else False
return self._info["hidden"] == 1
@property
def visible(self):
""" True if picture is visble """
return self._info["visible"]
@property
def intrash(self):
""" True if picture is in trash ('Recently Deleted' folder)"""
return self._info["intrash"]
@property
def date_trashed(self):
""" Date asset was placed in the trash or None """
# TODO: add add_timezone(dt, offset_seconds) to datetime_utils
# also update date_modified
trasheddate = self._info["trasheddate"]
if trasheddate:
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
delta = timedelta(seconds=seconds)
tz = timezone(delta)
return trasheddate.astimezone(tz=tz)
else:
return None
@property
def location(self):
""" returns (latitude, longitude) as float in degrees or None """
@@ -429,8 +581,8 @@ class PhotoInfo:
@property
def shared(self):
""" returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions """
"""returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions"""
if self._db._db_version > _PHOTOS_4_VERSION:
return self._info["shared"]
else:
@@ -438,43 +590,75 @@ class PhotoInfo:
@property
def uti(self):
""" Returns Uniform Type Identifier (UTI) for the image
for example: public.jpeg or com.apple.quicktime-movie
"""Returns Uniform Type Identifier (UTI) for the image
for example: public.jpeg or com.apple.quicktime-movie
"""
return self._info["UTI"]
if self._db._db_version <= _PHOTOS_4_VERSION and self.hasadjustments:
return self._info["UTI_edited"]
elif (
self._db._db_version <= _PHOTOS_4_VERSION
and self.has_raw
and self.raw_original
):
# return UTI of the non-raw image to match Photos 5+ behavior
return self._info["raw_pair_info"]["UTI"]
else:
return self._info["UTI"]
@property
def uti_original(self):
"""Returns Uniform Type Identifier (UTI) for the original image
for example: public.jpeg or com.apple.quicktime-movie
"""
if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
return self._info["raw_pair_info"]["UTI"]
elif self.shared:
# TODO: need reliable way to get original UTI for shared
return self.uti
else:
return self._info["UTI_original"]
@property
def uti_edited(self):
"""Returns Uniform Type Identifier (UTI) for the edited image
if the photo has been edited, otherwise None;
for example: public.jpeg
"""
if self._db._db_version >= _PHOTOS_5_VERSION:
return self.uti if self.hasadjustments else None
else:
return self._info["UTI_edited"]
@property
def uti_raw(self):
""" Returns Uniform Type Identifier (UTI) for the RAW image if there is one
for example: com.canon.cr2-raw-image
Returns None if no associated RAW image
"""Returns Uniform Type Identifier (UTI) for the RAW image if there is one
for example: com.canon.cr2-raw-image
Returns None if no associated RAW image
"""
return self._info["UTI_raw"]
@property
def ismovie(self):
""" Returns True if file is a movie, otherwise False
"""
return True if self._info["type"] == _MOVIE_TYPE else False
"""Returns True if file is a movie, otherwise False"""
return self._info["type"] == _MOVIE_TYPE
@property
def isphoto(self):
""" Returns True if file is an image, otherwise False
"""
return True if self._info["type"] == _PHOTO_TYPE else False
"""Returns True if file is an image, otherwise False"""
return self._info["type"] == _PHOTO_TYPE
@property
def incloud(self):
""" Returns True if photo is cloud asset and is synched to cloud
False if photo is cloud asset and not yet synched to cloud
None if photo is not cloud asset
"""Returns True if photo is cloud asset and is synched to cloud
False if photo is cloud asset and not yet synched to cloud
None if photo is not cloud asset
"""
return self._info["incloud"]
@property
def iscloudasset(self):
""" Returns True if photo is a cloud asset (in an iCloud library),
otherwise False
"""Returns True if photo is a cloud asset (in an iCloud library),
otherwise False
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return (
@@ -486,6 +670,11 @@ class PhotoInfo:
else:
return True if self._info["cloudAssetGUID"] is not None else False
@property
def isreference(self):
""" Returns True if photo is a reference (not copied to the Photos library), otherwise False """
return self._info["isreference"]
@property
def burst(self):
""" Returns True if photo is part of a Burst photo set, otherwise False """
@@ -493,9 +682,9 @@ class PhotoInfo:
@property
def burst_photos(self):
""" If photo is a burst photo, returns list of PhotoInfo objects
that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list """
"""If photo is a burst photo, returns list of PhotoInfo objects
that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list"""
if self._info["burst"]:
burst_uuid = self._info["burstUUID"]
return [
@@ -513,9 +702,9 @@ class PhotoInfo:
@property
def path_live_photo(self):
""" Returns path to the associated video file for a live photo
If photo is not a live photo, returns None
If photo is missing, returns None """
"""Returns path to the associated video file for a live photo
If photo is not a live photo, returns None
If photo is missing, returns None"""
photopath = None
if self._db._db_version <= _PHOTOS_4_VERSION:
@@ -632,30 +821,107 @@ class PhotoInfo:
@property
def has_raw(self):
""" returns True if photo has an associated RAW image, otherwise False """
""" returns True if photo has an associated raw image (that is, it's a RAW+JPEG pair), otherwise False """
return self._info["has_raw"]
@property
def israw(self):
""" returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw """
return "raw-image" in self.uti_original
@property
def raw_original(self):
""" returns True if associated RAW image and the RAW image is selected in Photos
via "Use RAW as Original "
otherwise returns False """
"""returns True if associated raw image and the raw image is selected in Photos
via "Use RAW as Original "
otherwise returns False"""
return self._info["raw_is_original"]
def render_template(self, template_str, none_str="_", path_sep=None):
@property
def height(self):
""" returns height of the current photo version in pixels """
return self._info["height"]
@property
def width(self):
""" returns width of the current photo version in pixels """
return self._info["width"]
@property
def orientation(self):
""" returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined """
if self._db._db_version <= _PHOTOS_4_VERSION:
return self._info["orientation"]
# For Photos 5+, try to get the adjusted orientation
if self.hasadjustments:
if self.adjustments:
return self.adjustments.adj_orientation
else:
# can't reliably determine orientation for edited photo if adjustmentinfo not available
return 0
else:
return self._info["orientation"]
@property
def original_height(self):
""" returns height of the original photo version in pixels """
return self._info["original_height"]
@property
def original_width(self):
""" returns width of the original photo version in pixels """
return self._info["original_width"]
@property
def original_orientation(self):
""" returns EXIF orientation of the original photo version as int """
return self._info["original_orientation"]
@property
def original_filesize(self):
""" returns filesize of original photo in bytes as int """
return self._info["original_filesize"]
def render_template(
self,
template_str,
none_str="_",
path_sep=None,
expand_inplace=False,
inplace_sep=None,
filename=False,
dirname=False,
strip=False,
):
"""Renders a template string for PhotoInfo instance using PhotoTemplate
Args:
template_str: a template string with fields to render
none_str: a str to use if template field renders to None, default is "_".
path_sep: a single character str to use as path separator when joining
path_sep: a single character str to use as path separator when joining
fields like folder_album; if not provided, defaults to os.path.sep
expand_inplace: expand multi-valued substitutions in-place as a single string
instead of returning individual strings
inplace_sep: optional string to use as separator between multi-valued keywords
with expand_inplace; default is ','
filename: if True, template output will be sanitized to produce valid file name
dirname: if True, template output will be sanitized to produce valid directory name
strip: if True, strips leading/trailing white space from resulting template
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
"""
template = PhotoTemplate(self)
return template.render(template_str, none_str, path_sep)
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
return template.render(
template_str,
none_str=none_str,
path_sep=path_sep,
expand_inplace=expand_inplace,
inplace_sep=inplace_sep,
filename=filename,
dirname=dirname,
strip=strip,
)
@property
def _longitude(self):
@@ -668,11 +934,11 @@ class PhotoInfo:
return self._info["latitude"]
def _get_album_uuids(self):
""" Return list of album UUIDs this photo is found in
"""Return list of album UUIDs this photo is found in
Filters out albums in the trash and any special album types
Returns: list of album UUIDs
Returns: list of album UUIDs
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
version4 = True
@@ -754,25 +1020,34 @@ class PhotoInfo:
"exif": exif,
"score": score,
"intrash": self.intrash,
"height": self.height,
"width": self.width,
"orientation": self.orientation,
"original_height": self.original_height,
"original_width": self.original_width,
"original_orientation": self.original_orientation,
"original_filesize": self.original_filesize,
}
return yaml.dump(info, sort_keys=False)
def json(self):
""" return JSON representation """
def asdict(self):
""" return dict representation """
date_modified_iso = (
self.date_modified.isoformat() if self.date_modified else None
)
folders = {album.title: album.folder_names for album in self.album_info}
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
place = self.place.as_dict() if self.place else {}
place = self.place.asdict() if self.place else {}
score = dataclasses.asdict(self.score) if self.score else {}
comments = [comment.asdict() for comment in self.comments]
likes = [like.asdict() for like in self.likes]
faces = [face.asdict() for face in self.face_info]
search_info = self.search_info.asdict() if self.search_info else {}
pic = {
return {
"library": self._db._library_path,
"uuid": self.uuid,
"filename": self.filename,
"original_filename": self.original_filename,
"date": self.date.isoformat(),
"date": self.date,
"description": self.description,
"title": self.title,
"keywords": self.keywords,
@@ -781,6 +1056,7 @@ class PhotoInfo:
"albums": self.albums,
"folders": folders,
"persons": self.persons,
"faces": faces,
"path": self.path,
"ismissing": self.ismissing,
"hasadjustments": self.hasadjustments,
@@ -794,12 +1070,14 @@ class PhotoInfo:
"isphoto": self.isphoto,
"ismovie": self.ismovie,
"uti": self.uti,
"uti_original": self.uti_original,
"burst": self.burst,
"live_photo": self.live_photo,
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"incloud": self.incloud,
"date_modified": date_modified_iso,
"isreference": self.isreference,
"date_modified": self.date_modified,
"portrait": self.portrait,
"screenshot": self.screenshot,
"slow_mo": self.slow_mo,
@@ -808,14 +1086,34 @@ class PhotoInfo:
"selfie": self.selfie,
"panorama": self.panorama,
"has_raw": self.has_raw,
"israw": self.israw,
"raw_original": self.raw_original,
"uti_raw": self.uti_raw,
"path_raw": self.path_raw,
"place": place,
"exif": exif,
"score": score,
"intrash": self.intrash,
"height": self.height,
"width": self.width,
"orientation": self.orientation,
"original_height": self.original_height,
"original_width": self.original_width,
"original_orientation": self.original_orientation,
"original_filesize": self.original_filesize,
"comments": comments,
"likes": likes,
"search_info": search_info,
}
return json.dumps(pic)
def json(self):
""" Return JSON representation """
def default(o):
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()
return json.dumps(self.asdict(), sort_keys=True, default=default)
def __eq__(self, other):
""" Compare two PhotoInfo objects for equality """

1278
osxphotos/photokit.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,4 @@ Processes a Photos.app library database to extract information about photos
"""
from .photosdb import PhotosDB
from .photosdb_utils import get_db_version, get_db_model_version, get_model_version

View File

@@ -0,0 +1,157 @@
""" PhotosDB method for processing comments and likes on shared photos.
Do not import this module directly """
import dataclasses
import datetime
from dataclasses import dataclass
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION, TIME_DELTA
from ..utils import _open_sql_file, normalize_unicode
def _process_comments(self):
""" load the comments and likes data from the database
this is a PhotosDB method that should be imported in
the PhotosDB class definition in photosdb.py
"""
self._db_hashed_person_id = {}
self._db_comments_uuid = {}
if self._db_version <= _PHOTOS_4_VERSION:
_process_comments_4(self)
else:
_process_comments_5(self)
@dataclass
class CommentInfo:
""" Class for shared photo comments """
datetime: datetime.datetime
user: str
ismine: bool
text: str
def asdict(self):
return dataclasses.asdict(self)
@dataclass
class LikeInfo:
""" Class for shared photo likes """
datetime: datetime.datetime
user: str
ismine: bool
def asdict(self):
return dataclasses.asdict(self)
# The following methods do not get imported into PhotosDB
# but will get called by _process_comments
def _process_comments_4(photosdb):
""" process comments and likes info for Photos <= 4
photosdb: PhotosDB instance """
raise NotImplementedError(
f"Not implemented for database version {photosdb._db_version}."
)
def _process_comments_5(photosdb):
""" process comments and likes info for Photos >= 5
photosdb: PhotosDB instance """
db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db)
results = conn.execute(
"""
SELECT DISTINCT
ZINVITEEHASHEDPERSONID,
ZINVITEEFIRSTNAME,
ZINVITEELASTNAME,
ZINVITEEFULLNAME
FROM
ZCLOUDSHAREDALBUMINVITATIONRECORD
"""
)
# order of results
# 0: ZINVITEEHASHEDPERSONID,
# 1: ZINVITEEFIRSTNAME,
# 2: ZINVITEELASTNAME,
# 3: ZINVITEEFULLNAME
photosdb._db_hashed_person_id = {}
for row in results.fetchall():
person_id = row[0]
photosdb._db_hashed_person_id[person_id] = {
"first_name": normalize_unicode(row[1]),
"last_name": normalize_unicode(row[2]),
"full_name": normalize_unicode(row[3]),
}
results = conn.execute(
f"""
SELECT
{asset_table}.ZUUID, -- UUID of the photo
ZCLOUDSHAREDCOMMENT.ZISLIKE, -- comment is actually a "like"
ZCLOUDSHAREDCOMMENT.ZCOMMENTDATE, -- date of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTTEXT, -- text of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTERHASHEDPERSONID, -- hashed ID of person who made comment/like
ZCLOUDSHAREDCOMMENT.ZISMYCOMMENT -- is my (this user's) comment
FROM ZCLOUDSHAREDCOMMENT
JOIN {asset_table} ON
{asset_table}.Z_PK = ZCLOUDSHAREDCOMMENT.ZCOMMENTEDASSET
OR
{asset_table}.Z_PK = ZCLOUDSHAREDCOMMENT.ZLIKEDASSET
"""
)
# order of results
# 0: ZGENERICASSET.ZUUID, -- UUID of the photo
# 1: ZCLOUDSHAREDCOMMENT.ZISLIKE, -- comment is actually a "like"
# 2: ZCLOUDSHAREDCOMMENT.ZCOMMENTDATE, -- date of comment
# 3: ZCLOUDSHAREDCOMMENT.ZCOMMENTTEXT, -- text of comment
# 4: ZCLOUDSHAREDCOMMENT.ZCOMMENTERHASHEDPERSONID, -- hashed ID of person who made comment/like
# 5: ZCLOUDSHAREDCOMMENT.ZISMYCOMMENT -- is my (this user's) comment
photosdb._db_comments_uuid = {}
for row in results:
uuid = row[0]
is_like = bool(row[1])
text = normalize_unicode(row[3])
try:
user_name = photosdb._db_hashed_person_id[row[4]]["full_name"]
except KeyError:
user_name = None
try:
dt = datetime.datetime.fromtimestamp(row[2] + TIME_DELTA)
except:
dt = datetime.datetime(1970, 1, 1)
ismine = bool(row[5])
try:
db_comments = photosdb._db_comments_uuid[uuid]
except KeyError:
photosdb._db_comments_uuid[uuid] = {"likes": [], "comments": []}
db_comments = photosdb._db_comments_uuid[uuid]
if is_like:
db_comments["likes"].append(LikeInfo(dt, user_name, ismine))
elif text:
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
# sort results
for uuid in photosdb._db_comments_uuid:
if photosdb._db_comments_uuid[uuid]["likes"]:
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
if photosdb._db_comments_uuid[uuid]["comments"]:
photosdb._db_comments_uuid[uuid]["comments"].sort(key=lambda x: x.datetime)
conn.close()

View File

@@ -3,9 +3,9 @@
import logging
from .._constants import _PHOTOS_4_VERSION
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _db_is_locked, _debug, _open_sql_file
from .photosdb_utils import get_db_version
def _process_exifinfo(self):
""" load the exif data from the database
@@ -34,15 +34,17 @@ def _process_exifinfo_5(photosdb):
photosdb: PhotosDB instance """
db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db)
result = conn.execute(
"""
SELECT ZGENERICASSET.ZUUID, ZEXTENDEDATTRIBUTES.*
FROM ZGENERICASSET
f"""
SELECT {asset_table}.ZUUID, ZEXTENDEDATTRIBUTES.*
FROM {asset_table}
JOIN ZEXTENDEDATTRIBUTES
ON ZEXTENDEDATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
ON ZEXTENDEDATTRIBUTES.ZASSET = {asset_table}.Z_PK
"""
)
@@ -54,3 +56,5 @@ def _process_exifinfo_5(photosdb):
if uuid in photosdb._db_exifinfo_uuid:
logging.warning(f"duplicate exifinfo record found for uuid {uuid}")
photosdb._db_exifinfo_uuid[uuid] = record
conn.close()

View File

@@ -0,0 +1,331 @@
""" Methods for PhotosDB to add Photos face info
"""
import logging
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _open_sql_file, normalize_unicode
from .photosdb_utils import get_db_version
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
Do not import this module directly
This module adds the following method to PhotosDB:
_process_faceinfo: process photo face info
The following data structures are added to PhotosDB
self._db_faceinfo_pk: {pk: {faceinfo}}
self._db_faceinfo_uuid: {photo uuid: [face pk]}
self._db_faceinfo_person: {person_pk: [face_pk]}
"""
def _process_faceinfo(self):
""" Process face information
"""
self._db_faceinfo_pk = {}
self._db_faceinfo_uuid = {}
self._db_faceinfo_person = {}
if self._db_version <= _PHOTOS_4_VERSION:
_process_faceinfo_4(self)
else:
_process_faceinfo_5(self)
def _process_faceinfo_4(photosdb):
""" Process face information for Photos 4 databases
Args:
photosdb: an OSXPhotosDB instance
"""
db = photosdb._tmp_db
(conn, cursor) = _open_sql_file(db)
result = cursor.execute(
"""
SELECT
RKFace.modelId,
RKVersion.uuid,
RKFace.uuid,
RKPerson.name,
RKFace.isInTrash,
RKFace.personId,
RKFace.imageModelId,
RKFace.sourceWidth,
RKFace.sourceHeight,
RKFace.centerX,
RKFace.centerY,
RKFace.size,
RKFace.leftEyeX,
RKFace.leftEyeY,
RKFace.rightEyeX,
RKFace.rightEyeY,
RKFace.mouthX,
RKFace.mouthY,
RKFace.hidden,
RKFace.manual,
RKFace.hasSmile,
RKFace.isLeftEyeClosed,
RKFace.isRightEyeClosed,
RKFace.poseRoll,
RKFace.poseYaw,
RKFace.posePitch,
RKFace.faceType,
RKFace.qualityMeasure
FROM
RKFace
JOIN RKPerson on RKPerson.modelId = RKFace.personId
JOIN RKVersion on RKVersion.modelId = RKFace.imageModelId
"""
)
# 0 RKFace.modelId,
# 1 RKVersion.uuid,
# 2 RKFace.uuid,
# 3 RKPerson.name,
# 4 RKFace.isInTrash,
# 5 RKFace.personId,
# 6 RKFace.imageModelId,
# 7 RKFace.sourceWidth,
# 8 RKFace.sourceHeight,
# 9 RKFace.centerX,
# 10 RKFace.centerY,
# 11 RKFace.size,
# 12 RKFace.leftEyeX,
# 13 RKFace.leftEyeY,
# 14 RKFace.rightEyeX,
# 15 RKFace.rightEyeY,
# 16 RKFace.mouthX,
# 17 RKFace.mouthY,
# 18 RKFace.hidden,
# 19 RKFace.manual,
# 20 RKFace.hasSmile,
# 21 RKFace.isLeftEyeClosed,
# 22 RKFace.isRightEyeClosed,
# 23 RKFace.poseRoll,
# 24 RKFace.poseYaw,
# 25 RKFace.posePitch,
# 26 RKFace.faceType,
# 27 RKFace.qualityMeasure
for row in result:
modelid = row[0]
asset_uuid = row[1]
person_id = row[5]
face = {}
face["pk"] = modelid
face["asset_uuid"] = asset_uuid
face["uuid"] = row[2]
face["person"] = person_id
face["fullname"] = normalize_unicode(row[3])
face["sourcewidth"] = row[7]
face["sourceheight"] = row[8]
face["centerx"] = row[9]
face["centery"] = row[10]
face["size"] = row[11]
face["lefteyex"] = row[12]
face["lefteyey"] = row[13]
face["righteyex"] = row[14]
face["righteyey"] = row[15]
face["mouthx"] = row[16]
face["mouthy"] = row[17]
face["hidden"] = row[18]
face["manual"] = row[19]
face["has_smile"] = row[20]
face["left_eye_closed"] = row[21]
face["right_eye_closed"] = row[22]
face["roll"] = row[23]
face["yaw"] = row[24]
face["pitch"] = row[25]
face["facetype"] = row[26]
face["quality"] = row[27]
# Photos 5 only
face["agetype"] = None
face["baldtype"] = None
face["eyemakeuptype"] = None
face["eyestate"] = None
face["facialhairtype"] = None
face["gendertype"] = None
face["glassestype"] = None
face["haircolortype"] = None
face["intrash"] = None
face["lipmakeuptype"] = None
face["smiletype"] = None
photosdb._db_faceinfo_pk[modelid] = face
try:
photosdb._db_faceinfo_uuid[asset_uuid].append(modelid)
except KeyError:
photosdb._db_faceinfo_uuid[asset_uuid] = [modelid]
try:
photosdb._db_faceinfo_person[person_id].append(modelid)
except KeyError:
photosdb._db_faceinfo_person[person_id] = [modelid]
conn.close()
def _process_faceinfo_5(photosdb):
""" Process face information for Photos 5 databases
Args:
photosdb: an OSXPhotosDB instance
"""
db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db)
result = cursor.execute(
f"""
SELECT
ZDETECTEDFACE.Z_PK,
{asset_table}.ZUUID,
ZDETECTEDFACE.ZUUID,
ZDETECTEDFACE.ZPERSON,
ZPERSON.ZFULLNAME,
ZDETECTEDFACE.ZAGETYPE,
ZDETECTEDFACE.ZBALDTYPE,
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
ZDETECTEDFACE.ZEYESSTATE,
ZDETECTEDFACE.ZFACIALHAIRTYPE,
ZDETECTEDFACE.ZGENDERTYPE,
ZDETECTEDFACE.ZGLASSESTYPE,
ZDETECTEDFACE.ZHAIRCOLORTYPE,
ZDETECTEDFACE.ZHASSMILE,
ZDETECTEDFACE.ZHIDDEN,
ZDETECTEDFACE.ZISINTRASH,
ZDETECTEDFACE.ZISLEFTEYECLOSED,
ZDETECTEDFACE.ZISRIGHTEYECLOSED,
ZDETECTEDFACE.ZLIPMAKEUPTYPE,
ZDETECTEDFACE.ZMANUAL,
ZDETECTEDFACE.ZQUALITYMEASURE,
ZDETECTEDFACE.ZSMILETYPE,
ZDETECTEDFACE.ZSOURCEHEIGHT,
ZDETECTEDFACE.ZSOURCEWIDTH,
ZDETECTEDFACE.ZBLURSCORE,
ZDETECTEDFACE.ZCENTERX,
ZDETECTEDFACE.ZCENTERY,
ZDETECTEDFACE.ZLEFTEYEX,
ZDETECTEDFACE.ZLEFTEYEY,
ZDETECTEDFACE.ZMOUTHX,
ZDETECTEDFACE.ZMOUTHY,
ZDETECTEDFACE.ZPOSEYAW,
ZDETECTEDFACE.ZQUALITY,
ZDETECTEDFACE.ZRIGHTEYEX,
ZDETECTEDFACE.ZRIGHTEYEY,
ZDETECTEDFACE.ZROLL,
ZDETECTEDFACE.ZSIZE,
ZDETECTEDFACE.ZYAW,
ZDETECTEDFACE.ZMASTERIDENTIFIER
FROM ZDETECTEDFACE
JOIN {asset_table} ON {asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
JOIN ZPERSON ON ZPERSON.Z_PK = ZDETECTEDFACE.ZPERSON;
"""
)
# 0 ZDETECTEDFACE.Z_PK
# 1 ZGENERICASSET.ZUUID,
# 2 ZDETECTEDFACE.ZUUID,
# 3 ZDETECTEDFACE.ZPERSON,
# 4 ZPERSON.ZFULLNAME,
# 5 ZDETECTEDFACE.ZAGETYPE,
# 6 ZDETECTEDFACE.ZBALDTYPE,
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
# 8 ZDETECTEDFACE.ZEYESSTATE,
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
# 10 ZDETECTEDFACE.ZGENDERTYPE,
# 11 ZDETECTEDFACE.ZGLASSESTYPE,
# 12 ZDETECTEDFACE.ZHAIRCOLORTYPE,
# 13 ZDETECTEDFACE.ZHASSMILE,
# 14 ZDETECTEDFACE.ZHIDDEN,
# 15 ZDETECTEDFACE.ZISINTRASH,
# 16 ZDETECTEDFACE.ZISLEFTEYECLOSED,
# 17 ZDETECTEDFACE.ZISRIGHTEYECLOSED,
# 18 ZDETECTEDFACE.ZLIPMAKEUPTYPE,
# 19 ZDETECTEDFACE.ZMANUAL,
# 20 ZDETECTEDFACE.ZQUALITYMEASURE,
# 21 ZDETECTEDFACE.ZSMILETYPE,
# 22 ZDETECTEDFACE.ZSOURCEHEIGHT,
# 23 ZDETECTEDFACE.ZSOURCEWIDTH,
# 24 ZDETECTEDFACE.ZBLURSCORE,
# 25 ZDETECTEDFACE.ZCENTERX,
# 26 ZDETECTEDFACE.ZCENTERY,
# 27 ZDETECTEDFACE.ZLEFTEYEX,
# 28 ZDETECTEDFACE.ZLEFTEYEY,
# 29 ZDETECTEDFACE.ZMOUTHX,
# 30 ZDETECTEDFACE.ZMOUTHY,
# 31 ZDETECTEDFACE.ZPOSEYAW,
# 32 ZDETECTEDFACE.ZQUALITY,
# 33 ZDETECTEDFACE.ZRIGHTEYEX,
# 34 ZDETECTEDFACE.ZRIGHTEYEY,
# 35 ZDETECTEDFACE.ZROLL,
# 36 ZDETECTEDFACE.ZSIZE,
# 37 ZDETECTEDFACE.ZYAW,
# 38 ZDETECTEDFACE.ZMASTERIDENTIFIER
for row in result:
pk = row[0]
asset_uuid = row[1]
person_pk = row[3]
face = {}
face["pk"] = pk
face["asset_uuid"] = asset_uuid
face["uuid"] = row[2]
face["person"] = person_pk
face["fullname"] = normalize_unicode(row[4])
face["agetype"] = row[5]
face["baldtype"] = row[6]
face["eyemakeuptype"] = row[7]
face["eyestate"] = row[8]
face["facialhairtype"] = row[9]
face["gendertype"] = row[10]
face["glassestype"] = row[11]
face["haircolortype"] = row[12]
face["has_smile"] = row[13]
face["hidden"] = row[14]
face["intrash"] = row[15]
face["left_eye_closed"] = row[16]
face["right_eye_closed"] = row[17]
face["lipmakeuptype"] = row[18]
face["manual"] = row[19]
face["smiletype"] = row[21]
face["sourceheight"] = row[22]
face["sourcewidth"] = row[23]
face["facetype"] = None # Photos 4 only
face["centerx"] = row[25]
face["centery"] = row[26]
face["lefteyex"] = row[27]
face["lefteyey"] = row[28]
face["mouthx"] = row[29]
face["mouthy"] = row[30]
face["quality"] = row[32]
face["righteyex"] = row[33]
face["righteyey"] = row[34]
face["roll"] = row[35]
face["size"] = row[36]
face["yaw"] = row[37]
face["pitch"] = 0.0 # not defined in Photos 5
photosdb._db_faceinfo_pk[pk] = face
try:
photosdb._db_faceinfo_uuid[asset_uuid].append(pk)
except KeyError:
photosdb._db_faceinfo_uuid[asset_uuid] = [pk]
try:
photosdb._db_faceinfo_person[person_pk].append(pk)
except KeyError:
photosdb._db_faceinfo_person[person_pk] = [pk]
conn.close()

View File

@@ -4,8 +4,9 @@
import logging
from .._constants import _PHOTOS_4_VERSION
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _open_sql_file
from .photosdb_utils import get_db_version
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
@@ -45,16 +46,18 @@ def _process_scoreinfo_5(photosdb):
db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db)
result = cursor.execute(
"""
f"""
SELECT
ZGENERICASSET.ZUUID,
ZGENERICASSET.ZOVERALLAESTHETICSCORE,
ZGENERICASSET.ZCURATIONSCORE,
ZGENERICASSET.ZPROMOTIONSCORE,
ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
{asset_table}.ZUUID,
{asset_table}.ZOVERALLAESTHETICSCORE,
{asset_table}.ZCURATIONSCORE,
{asset_table}.ZPROMOTIONSCORE,
{asset_table}.ZHIGHLIGHTVISIBILITYSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
@@ -78,8 +81,8 @@ def _process_scoreinfo_5(photosdb):
ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
FROM ZGENERICASSET
JOIN ZCOMPUTEDASSETATTRIBUTES ON ZCOMPUTEDASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
FROM {asset_table}
JOIN ZCOMPUTEDASSETATTRIBUTES ON ZCOMPUTEDASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
"""
)
@@ -143,3 +146,5 @@ def _process_scoreinfo_5(photosdb):
scores["well_framed_subject"] = row[26]
scores["well_timed_shot"] = row[27]
photosdb._db_scoreinfo_uuid[uuid] = scores
conn.close()

View File

@@ -10,7 +10,7 @@ import uuid as uuidlib
from pprint import pformat
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL
from ..utils import _db_is_locked, _debug, _open_sql_file
from ..utils import _db_is_locked, _debug, _open_sql_file, normalize_unicode
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
@@ -104,17 +104,19 @@ def _process_searchinfo(self):
for row in c:
uuid = ints_to_uuid(row[1], row[2])
# strings have null character appended, so strip it
record = {}
record["uuid"] = uuid
record["rowid"] = row[0]
record["uuid_0"] = row[1]
record["uuid_1"] = row[2]
record["groupid"] = row[3]
record["category"] = row[4]
record["owning_groupid"] = row[5]
record["content_string"] = row[6].replace("\x00", "")
record["normalized_string"] = row[7].replace("\x00", "")
record["lookup_identifier"] = row[8]
record = {
"uuid": uuid,
"rowid": row[0],
"uuid_0": row[1],
"uuid_1": row[2],
"groupid": row[3],
"category": row[4],
"owning_groupid": row[5],
"content_string": normalize_unicode(row[6].replace("\x00", "")),
}
record["normalized_string"] = normalize_unicode(row[7].replace("\x00", ""))
record["lookup_identifier"] = normalize_unicode(row[8].replace("\x00", ""))
try:
_db_searchinfo_uuid[uuid].append(record)
@@ -148,6 +150,8 @@ def _process_searchinfo(self):
+ pformat(self._db_searchinfo_labels_normalized)
)
conn.close()
@property
def labels(self):

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
""" utility functions used by PhotosDB """
import logging
import plistlib
from .._constants import (
_PHOTOS_5_MODEL_VERSION,
_PHOTOS_6_MODEL_VERSION,
_TESTED_DB_VERSIONS,
)
from ..utils import _open_sql_file
def get_db_version(db_file):
""" Gets the Photos DB version from LiGlobals table
Args:
db_file: path to photos.db database file containing LiGlobals table
Returns: version as str
"""
version = None
(conn, c) = _open_sql_file(db_file)
# get database version
c.execute("SELECT value from LiGlobals where LiGlobals.keyPath is 'libraryVersion'")
version = c.fetchone()[0]
conn.close()
if version not in _TESTED_DB_VERSIONS:
print(
f"WARNING: Only tested on database versions [{', '.join(_TESTED_DB_VERSIONS)}]"
+ f" You have database version={version} which has not been tested"
)
return version
def get_model_version(db_file):
""" Returns the database model version from Z_METADATA
Args:
db_file: path to Photos.sqlite database file containing Z_METADATA table
Returns: model version as str
"""
version = None
(conn, c) = _open_sql_file(db_file)
# get database version
c.execute("SELECT MAX(Z_VERSION), Z_PLIST FROM Z_METADATA")
results = c.fetchone()
conn.close()
plist = plistlib.loads(results[1])
return plist["PLModelVersion"]
def get_db_model_version(db_file):
""" Returns Photos version based on model version found in db_file
Args:
db_file: path to Photos.sqlite file
Returns: int of major Photos version number (e.g. 5 or 6).
If unknown model version found, logs warning and returns most current Photos version.
"""
model_ver = get_model_version(db_file)
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
db_ver = 5
elif _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
db_ver = 6
else:
logging.warning(f"Unknown model version: {model_ver}")
# cross our fingers and try latest version
db_ver = 6
return db_ver

View File

@@ -0,0 +1,94 @@
The templating system converts one or template statements, written in osxphotos templating language, to one or more rendered values using information from the photo being processed.
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
Template statements may contain one or more modifiers. The full syntax is:
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"`
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
`+`: If present before template `name`, expands the template in place. If `delim` not provided, values are joined with no delimiter.
e.g. if Photo keywords are `["foo","bar"]`:
- `"{keyword}"` renders to `"foo", "bar"`
- `"{,+keyword}"` renders to: `"foo,bar"`
- `"{; +keyword}"` renders to: `"foo; bar"`
- `"{+keyword}"` renders to `"foobar"`
`template_field`: The template field to resolve. See [Template Substitutions](#template-substitutions) for full list of template fields.
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
Valid filters are:
<!-- OSXPHOTOS-FILTER-TABLE:START - Do not remove or modify this section -->
- lower: Convert value to lower case, e.g. 'Value' => 'value'.
- upper: Convert value to upper case, e.g. 'Value' => 'VALUE'.
- strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
- titlecase: Convert value to title case, e.g. 'my value' => 'My Value'.
- capitalize: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
<!-- OSXPHOTOS-FILTER-TABLE:END -->
e.g. if Photo keywords are `["FOO","bar"]`:
- `"{keyword|lower}"` renders to `"foo", "bar"`
- `"{keyword|upper}"` renders to: `"FOO", "BAR"`
- `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
- `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
e.g. if Photo description is "my description":
- `"{descr|titlecase}"` renders to: `"My Description"`
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album}"` renders to `["Folder1/Album1"]`
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]`
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
e.g. if photo is an HDR image,
- `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
and if it is not an HDR image,
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
e.g., if photo has no title set,
- `"{title}"` renders to "_"
- `"{title,I have no title}"` renders to `"I have no title"`
Template fields such as `created.strftime` use the default value to pass the template to use for `strftime`.
e.g., if photo date is 4 February 2020, 19:07:38,
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
Some template fields such as `"{media_type}"` use the default value to allow customization of the output. For example, `"{media_type}"` resolves to the special media type of the photo such as `panorama` or `selfie`. You may use the default value to override these in form: `"{media_type,video=vidéo;time_lapse=vidéo_accélérée}"`. In this example, if photo was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
Either or both bool_value or default (False value) may be empty which would result in empty string `""` when rendered.
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.

File diff suppressed because it is too large Load Diff

122
osxphotos/phototemplate.tx Normal file
View File

@@ -0,0 +1,122 @@
// OSXPhotos Template Language (OTL)
// a TemplateString has format:
// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post
// a TemplateStatement may contain zero or more TemplateStrings
// The pre and post are optional strings
// The template itself (inside the {}) is also optional but if present
// everything but template_field is also optional
Statement:
(template_strings+=TemplateString)?
;
TemplateString:
pre=NON_TEMPLATE_STRING?
template=Template?
post=NON_TEMPLATE_STRING?
;
Template:
(
"{"
delim=Delim
field=Field
subfield=SubField
filter=Filter
pathsep=PathSep
findreplace=FindReplace
bool=Boolean
default=Default
"}"
)?
;
NON_TEMPLATE_STRING:
/[^\{\},]*/
;
Delim:
(
(value=DELIM_WORD)?
'+'
)?
;
DELIM_WORD:
/[^\{\}]*(?=\+\w)/
;
Field:
FIELD_WORD+
;
SubField:
(
":"-
SUBFIELD_WORD+
)?
;
FIELD_WORD:
/[\.\w]+/
;
SUBFIELD_WORD:
/[\.\w:]+/
;
Filter:
(
"|"-
(value+=FILTER_WORD['|'])?
)?
;
FILTER_WORD:
/[\.\w]+/
;
PathSep:
(
"("
(value=/[^\(\)\{\}]{0,1}/)?
")"
)?
;
FindReplace:
(
"["
(pairs+=FindReplacePair['|'])?
"]"
)?
;
FindReplacePair:
find=FIND_WORD
","
(replace=REPLACE_WORD)?
;
FIND_WORD:
/[^\[\]\|]*(?=\,)/
;
REPLACE_WORD:
/[^\[\]\|]*/
;
Boolean:
(
"?"
(value=Statement)?
)?
;
Default:
(
","
(value=Statement)?
)?
;

View File

@@ -11,6 +11,9 @@ from collections import namedtuple # pylint: disable=syntax-error
import yaml
from bpylist import archiver
from ._constants import UNICODE_FORMAT
from .utils import normalize_unicode
# postal address information, returned by PlaceInfo.address
PostalAddress = namedtuple(
"PostalAddress",
@@ -76,12 +79,12 @@ class PLRevGeoLocationInfo:
geoServiceProvider,
postalAddress,
):
self.addressString = addressString
self.addressString = normalize_unicode(addressString)
self.countryCode = countryCode
self.mapItem = mapItem
self.isHome = isHome
self.compoundNames = compoundNames
self.compoundSecondaryNames = compoundSecondaryNames
self.compoundNames = normalize_unicode(compoundNames)
self.compoundSecondaryNames = normalize_unicode(compoundSecondaryNames)
self.version = version
self.geoServiceProvider = geoServiceProvider
self.postalAddress = postalAddress
@@ -183,7 +186,7 @@ class PLRevGeoMapItemAdditionalPlaceInfo:
def __init__(self, area, name, placeType, dominantOrderType):
self.area = area
self.name = name
self.name = normalize_unicode(name)
self.placeType = placeType
self.dominantOrderType = dominantOrderType
@@ -232,13 +235,13 @@ class CNPostalAddress:
_subLocality,
):
self._ISOCountryCode = _ISOCountryCode
self._city = _city
self._country = _country
self._postalCode = _postalCode
self._state = _state
self._street = _street
self._subAdministrativeArea = _subAdministrativeArea
self._subLocality = _subLocality
self._city = normalize_unicode(_city)
self._country = normalize_unicode(_country)
self._postalCode = normalize_unicode(_postalCode)
self._state = normalize_unicode(_state)
self._street = normalize_unicode(_street)
self._subAdministrativeArea = normalize_unicode(_subAdministrativeArea)
self._subLocality = normalize_unicode(_subLocality)
def __eq__(self, other):
return all(
@@ -414,9 +417,9 @@ class PlaceInfo4(PlaceInfo):
# 2: type
# 3: area
try:
places_dict[p[2]].append((p[1], p[3]))
places_dict[p[2]].append((normalize_unicode(p[1]), p[3]))
except KeyError:
places_dict[p[2]] = [(p[1], p[3])]
places_dict[p[2]] = [(normalize_unicode(p[1]), p[3])]
# build list to populate PlaceNames tuple
# initialize with empty lists for each field in PlaceNames
@@ -488,7 +491,7 @@ class PlaceInfo4(PlaceInfo):
}
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self):
def asdict(self):
return {
"name": self.name,
"names": self.names._asdict(),
@@ -631,7 +634,7 @@ class PlaceInfo5(PlaceInfo):
}
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self):
def asdict(self):
return {
"name": self.name,
"names": self.names._asdict(),

View File

@@ -0,0 +1,8 @@
Templates for creating XMP sidecar files
* xmp_sidecar.mako: Mako template for generating XMP sidecar
* xmp_sidecar_beta.mako: template for beta testing new XMP properties, if run with --beta, osxphotos will use this template, otherwise it always uses xmp_sidecar.mako
Reference:
* https://www.adobe.com/devnet/xmp.html
* https://www.makotemplates.org/

View File

@@ -1,30 +1,51 @@
<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
<%def name="photoshop_sidecar_for_extension(extension)">
% if extension is None:
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
% else:
<photoshop:SidecarForExtension>${extension}</photoshop:SidecarForExtension>
% endif
</%def>
<%def name="dc_description(desc)">
% if desc is None:
<dc:description></dc:description>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang='x-default'/>
</rdf:Alt>
</dc:description>
% else:
<dc:description>${desc}</dc:description>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang='x-default'>${desc | x}</rdf:li>
</rdf:Alt>
</dc:description>
% endif
</%def>
<%def name="dc_title(title)">
% if title is None:
<dc:title></dc:title>
<dc:title>
<rdf:Alt>
<rdf:li xml:lang='x-default'/>
</rdf:Alt>
</dc:title>
% else:
<dc:title>${title}</dc:title>
<dc:title>
<rdf:Alt>
<rdf:li xml:lang='x-default'>${title | x}</rdf:li>
</rdf:Alt>
</dc:title>
% endif
</%def>
<%def name="dc_subject(subject)">
% if subject:
<!-- keywords and persons listed in <dc:subject> as Photos does -->
<dc:subject>
<rdf:Seq>
% for subj in subject:
<rdf:li>${subj}</rdf:li>
% endfor
</rdf:Seq>
<rdf:Bag>
% for subj in subject:
<rdf:li>${subj | x}</rdf:li>
% endfor
</rdf:Bag>
</dc:subject>
% endif
</%def>
@@ -38,11 +59,11 @@
<%def name="iptc_personinimage(persons)">
% if persons:
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
% for person in persons:
<rdf:li>${person}</rdf:li>
% endfor
</rdf:Bag>
<rdf:Bag>
% for person in persons:
<rdf:li>${person | x}</rdf:li>
% endfor
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
% endif
</%def>
@@ -50,11 +71,11 @@
<%def name="dk_tagslist(keywords)">
% if keywords:
<digiKam:TagsList>
<rdf:Seq>
% for keyword in keywords:
<rdf:li>${keyword}</rdf:li>
% endfor
</rdf:Seq>
<rdf:Seq>
% for keyword in keywords:
<rdf:li>${keyword | x}</rdf:li>
% endfor
</rdf:Seq>
</digiKam:TagsList>
% endif
</%def>
@@ -71,29 +92,108 @@
% endif
</%def>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${dc_description(photo.description)}
${dc_title(photo.title)}
${dc_subject(subjects)}
${dc_datecreated(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
${iptc_personinimage(persons)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
${dk_tagslist(keywords)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<%def name="gps_info(latitude, longitude)">
% if latitude is not None and longitude is not None:
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
<exif:GPSLatitude>${int(abs(latitude))},${(abs(latitude) % 1) * 60}${"N" if latitude >= 0 else "S"}</exif:GPSLatitude>
% endif
</%def>
<%def name="mwg_face_regions(photo)">
% if photo.face_info:
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>
<rdf:Bag>
% for face in photo.face_info:
<rdf:li rdf:parseType="Resource">
<mwg-rs:Area rdf:parseType="Resource">
<stArea:h>${'{0:.6f}'.format(face.mwg_rs_area.h)}</stArea:h>
<stArea:w>${'{0:.6f}'.format(face.mwg_rs_area.w)}</stArea:w>
<stArea:x>${'{0:.6f}'.format(face.mwg_rs_area.x)}</stArea:x>
<stArea:y>${'{0:.6f}'.format(face.mwg_rs_area.y)}</stArea:y>
<stArea:unit>normalized</stArea:unit>
</mwg-rs:Area>
<mwg-rs:Name>${face.name}</mwg-rs:Name>
<mwg-rs:Rotation>${face.roll}</mwg-rs:Rotation>
<mwg-rs:Type>Face</mwg-rs:Type>
</rdf:li>
% endfor
</rdf:Bag>
</mwg-rs:RegionList>
</mwg-rs:Regions>
% endif
</%def>
<%def name="mpri_face_regions(photo)">
% if photo.face_info:
<MP:RegionInfo rdf:parseType="Resource">
<MPRI:Regions>
<rdf:Bag>
% for face in photo.face_info:
<rdf:li rdf:parseType="Resource">
<MPReg:PersonDisplayName>${face.name}</MPReg:PersonDisplayName>
<MPReg:Rectangle>${'{0:.6f}'.format(face.mpri_reg_rect.x)}, ${'{0:.6f}'.format(face.mpri_reg_rect.y)}, ${'{0:.6f}'.format(face.mpri_reg_rect.h)}, ${'{0:.6f}'.format(face.mpri_reg_rect.w)}</MPReg:Rectangle>
</rdf:li>
% endfor
</rdf:Bag>
</MPRI:Regions>
</MP:RegionInfo>
% endif
</%def>
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${photoshop_sidecar_for_extension(extension)}
${dc_description(description)}
${dc_title(photo.title)}
${dc_subject(subjects)}
${dc_datecreated(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
${iptc_personinimage(persons)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
${dk_tagslist(keywords)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
${gps_info(*location)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
${mwg_face_regions(photo)}
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:MP="http://ns.microsoft.com/photo/1.2/"
xmlns:MPRI="http://ns.microsoft.com/photo/1.2/t/RegionInfo#"
xmlns:MPReg="http://ns.microsoft.com/photo/1.2/t/Region#">
${mpri_face_regions(photo)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>

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