Compare commits

...

273 Commits

Author SHA1 Message Date
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
662 changed files with 24856 additions and 5550 deletions

173
.all-contributorsrc Normal file
View File

@@ -0,0 +1,173 @@
{
"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"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

@@ -6,6 +6,7 @@ jobs:
build:
runs-on: macOS-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
strategy:
max-parallel: 4
matrix:

View File

@@ -4,6 +4,562 @@ 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.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

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

1346
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>`_

3
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"

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"

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')]
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 )

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,8 @@ 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()
@@ -58,8 +60,16 @@ _DB_TABLE_NAMES = {
},
}
# which major version operating systems have been tested
_TESTED_OS_VERSIONS = ["12", "13", "14", "15", "16"]
# which version operating systems have been tested
_TESTED_OS_VERSIONS = [
("10", "12"),
("10", "13"),
("10", "14"),
("10", "15"),
("10", "16"),
("11", "0"),
("11", "1"),
]
# Photos 5 has persons who are empty string if unidentified face
_UNKNOWN_PERSON = "_UNKNOWN_"
@@ -79,6 +89,7 @@ _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
@@ -102,6 +113,63 @@ _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
@@ -109,3 +177,32 @@ 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]

View File

@@ -1,4 +1,3 @@
""" version info """
__version__ = "0.36.2"
__version__ = "0.40.3"

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())}

View File

@@ -1,10 +1,10 @@
""" datetime utilities """
""" 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
""" Return local timezone as datetime.timezone tzinfo for dt
Args:
dt: datetime.datetime
@@ -21,21 +21,18 @@ def get_local_tz(dt):
raise ValueError("dt must be naive datetime.datetime object")
def datetime_remove_tz(dt):
""" remove timezone from a datetime.datetime object
dt: datetime.datetime object with tzinfo
returns: dt without any timezone info (naive 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_has_tz(dt):
""" return True if datetime dt has tzinfo else False
""" Return True if datetime dt has tzinfo else False
Args:
dt: datetime.datetime
returns True if dt is timezone aware, else False """
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)}")
@@ -43,11 +40,90 @@ def datetime_has_tz(dt):
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def datetime_naive_to_local(dt):
""" convert naive (timezone unaware) datetime.datetime
to aware timezone in local timezone
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 local 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)}")
@@ -60,3 +136,26 @@ def datetime_naive_to_local(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,19 +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)
@@ -23,9 +22,7 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
exiftool_path = shutil.which('exiftool')
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,19 +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 (possible interfere w/ --touch-file)
"-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")
@@ -135,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")
@@ -178,25 +215,51 @@ 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:
command_str = b"\n".join([f.encode("utf-8") for f in self.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
@@ -204,18 +267,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):
@@ -225,23 +297,35 @@ 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 asdict(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()
exifdict = json.loads(json_str)
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 """
@@ -250,3 +334,15 @@ class ExifTool:
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 = "2.0"
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
class ExportDB_ABC(ABC):
@@ -76,6 +76,14 @@ class ExportDB_ABC(ABC):
def set_exifdata_for_file(self, uuid, exifdata):
pass
@abstractmethod
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
pass
@abstractmethod
def get_sidecar_for_file(self, filename):
pass
@abstractmethod
def set_data(
self,
@@ -141,6 +149,12 @@ class ExportDBNoOp(ExportDB_ABC):
def set_exifdata_for_file(self, uuid, exifdata):
pass
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 set_data(
self,
filename,
@@ -379,6 +393,48 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
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)
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 set_data(
self,
filename,
@@ -479,13 +535,11 @@ class ExportDB(ExportDB_ABC):
if not os.path.isfile(dbfile):
conn = self._get_db_connection(dbfile)
if conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
if not conn:
raise Exception("Error getting connection to database {dbfile}")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else:
conn = self._get_db_connection(dbfile)
self.was_created = False
@@ -495,8 +549,7 @@ class ExportDB(ExportDB_ABC):
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
def _get_db_connection(self, dbfile):
@@ -570,11 +623,20 @@ class ExportDB(ExportDB_ABC):
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()

View File

@@ -1,6 +1,5 @@
""" FileUtil class with methods for copy, hardlink, unlink, etc. """
import logging
import os
import pathlib
import stat
@@ -8,8 +7,11 @@ import subprocess
import sys
from abc import ABC, abstractmethod
import CoreFoundation
from .imageconverter import ImageConverter
class FileUtilABC(ABC):
""" Abstract base class for FileUtil """
@@ -28,6 +30,11 @@ class FileUtilABC(ABC):
def unlink(cls, dest):
pass
@classmethod
@abstractmethod
def rmdir(cls, dest):
pass
@classmethod
@abstractmethod
def utime(cls, path, times):
@@ -53,6 +60,11 @@ class FileUtilABC(ABC):
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 """
@@ -74,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):
@@ -119,6 +130,14 @@ class FileUtilMacOS(FileUtilABC):
else:
os.unlink(filepath)
@classmethod
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. """
@@ -169,7 +188,7 @@ class FileUtilMacOS(FileUtilABC):
def file_sig(cls, f1):
""" 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
@@ -183,7 +202,24 @@ class FileUtilMacOS(FileUtilABC):
True if success, otherwise False
"""
converter = ImageConverter()
return converter.write_jpeg(src_file, dest_file, compression_quality=compression_quality)
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):
@@ -194,6 +230,7 @@ class FileUtilMacOS(FileUtilABC):
# 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):
""" Various file utilities """
@@ -233,6 +270,10 @@ 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}")
@@ -245,3 +286,7 @@ class FileUtilNoOp(FileUtil):
@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}")

View File

@@ -1,12 +1,12 @@
""" ImageConverter class
Convert an image to JPEG using CoreImage --
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 logging
import pathlib
import objc
import Metal
import Quartz
from Cocoa import NSURL
@@ -16,6 +16,12 @@ from Foundation import NSDictionary
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
@@ -60,53 +66,61 @@ class ImageConverter:
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
"""
# accept input_path or output_path as pathlib.Path
if not isinstance(input_path, str):
input_path = str(input_path)
# 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 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 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}"
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
)
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:
logging.debug(f"Could not create CIImage for {input_path}")
return False
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:
logging.debug(
"Error converting file {input_path} to jpeg at {output_path}: {error}"
output_options = NSDictionary.dictionaryWithDictionary_(
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
)
return False
(
_,
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}"
)

View File

@@ -1,8 +1,9 @@
""" utility functions for validating/sanitizing path components """
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
import pathvalidate
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
def sanitize_filepath(filepath):
""" sanitize a filepath """

View File

@@ -4,6 +4,11 @@ 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
@@ -216,6 +221,62 @@ class FaceInfo:
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
@@ -259,6 +320,43 @@ class FaceInfo:
_, _, 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
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
@@ -273,22 +371,11 @@ class FaceInfo:
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
orientation = self.photo.orientation
x, y = xy
x, y = self._fix_orientation(xy)
dx = self.photo.width
dy = self.photo.height
if orientation in [1, 2]:
y = 1.0 - y
elif orientation in [3, 4]:
x = 1.0 - x
elif orientation in [5, 6]:
x, y = 1.0 - y, 1.0 - x
if orientation in [5, 6, 7, 8]:
dx, dy = dy, dx
elif orientation in [7, 8]:
x, y = y, x
dx, dy = dy, dx
else:
logging.warning(f"Unhandled orientation: {orientation}")
return (int(x * dx), int(y * dy))
def _make_point_with_rotation(self, xy):
@@ -336,6 +423,8 @@ class FaceInfo:
"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,

View File

@@ -18,12 +18,11 @@ 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

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

@@ -21,11 +21,11 @@ from .._constants import (
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_VERSION,
_PHOTOS_5_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, ImportInfo
from ..personinfo import FaceInfo, PersonInfo
@@ -43,6 +43,7 @@ class PhotoInfo:
# import additional methods
from ._photoinfo_searchinfo import (
search_info,
search_info_normalized,
labels,
labels_normalized,
SearchInfo,
@@ -53,14 +54,17 @@ 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
from ._photoinfo_comments import comments, likes
def __init__(self, db=None, uuid=None, info=None):
self._uuid = uuid
@@ -70,7 +74,11 @@ class PhotoInfo:
@property
def filename(self):
""" filename of the picture """
if self._db._db_version <= _PHOTOS_4_VERSION and self.has_raw and self.raw_original:
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:
@@ -78,13 +86,18 @@ class PhotoInfo:
@property
def original_filename(self):
""" 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:
"""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
return self._info["raw_pair_info"]["originalFilename"]
original_name = self._info["raw_pair_info"]["originalFilename"]
else:
return self._info["originalFilename"]
original_name = self._info["originalFilename"]
return original_name or self.filename
@property
def date(self):
@@ -93,8 +106,16 @@ class PhotoInfo:
@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
@@ -117,6 +138,7 @@ class PhotoInfo:
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
@@ -147,6 +169,8 @@ class PhotoInfo:
photopath = os.path.join(
self._db._masters_path, self._info["imagePath"]
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
@@ -158,6 +182,8 @@ class PhotoInfo:
self._info["directory"],
self._info["filename"],
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
@@ -171,6 +197,8 @@ class PhotoInfo:
self._info["directory"],
self._info["filename"],
)
if not os.path.isfile(photopath):
photopath = None
self._path = photopath
return photopath
@@ -179,18 +207,15 @@ class PhotoInfo:
""" 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()
return self._path_edited
else:
self._path_edited = self._path_edited_5()
return self._path_edited
return self._path_edited
def _path_edited_5(self):
""" return path_edited for Photos >= 5 """
@@ -248,8 +273,6 @@ class PhotoInfo:
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
# logging.debug(photopath)
return photopath
def _path_edited_4(self):
@@ -282,7 +305,7 @@ class PhotoInfo:
# 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,
library, "resources", "media", "version", folder_id, "00", filename
)
if not os.path.isfile(photopath):
@@ -470,46 +493,61 @@ 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 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 """
@@ -517,8 +555,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:
@@ -526,24 +564,25 @@ 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
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
if self.hasadjustments:
return self._info["UTI_edited"]
elif 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"]
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
"""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"]
@@ -555,9 +594,9 @@ class PhotoInfo:
@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
"""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
@@ -566,36 +605,34 @@ class PhotoInfo:
@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 (
@@ -607,6 +644,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 """
@@ -614,9 +656,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 [
@@ -634,9 +676,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:
@@ -763,9 +805,9 @@ class PhotoInfo:
@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"]
@property
@@ -812,27 +854,27 @@ class PhotoInfo:
inplace_sep=None,
filename=False,
dirname=False,
replacement=":",
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
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
replacement: str, value to replace any illegal file path characters with; default = ":"
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)
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
return template.render(
template_str,
none_str=none_str,
@@ -841,7 +883,7 @@ class PhotoInfo:
inplace_sep=inplace_sep,
filename=filename,
dirname=dirname,
replacement=replacement
strip=strip,
)
@property
@@ -855,11 +897,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
@@ -961,6 +1003,7 @@ class PhotoInfo:
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 {}
return {
"library": self._db._library_path,
@@ -996,6 +1039,7 @@ class PhotoInfo:
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"incloud": self.incloud,
"isreference": self.isreference,
"date_modified": self.date_modified,
"portrait": self.portrait,
"screenshot": self.screenshot,
@@ -1022,10 +1066,12 @@ class PhotoInfo:
"original_filesize": self.original_filesize,
"comments": comments,
"likes": likes,
"search_info": search_info,
}
def json(self):
""" Return JSON representation """
def default(o):
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()

1278
osxphotos/photokit.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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"] = normalize_unicode(row[6].replace("\x00", ""))
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"] = row[8]
record["lookup_identifier"] = normalize_unicode(row[8].replace("\x00", ""))
try:
_db_searchinfo_uuid[uuid].append(record)

View File

@@ -12,7 +12,6 @@ import sys
import tempfile
from datetime import datetime, timedelta, timezone
from pprint import pformat
from shutil import copyfile
from .._constants import (
_DB_TABLE_NAMES,
@@ -35,6 +34,7 @@ from .._constants import (
from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
from ..fileutil import FileUtil
from ..personinfo import PersonInfo
from ..photoinfo import PhotoInfo
from ..utils import (
@@ -70,12 +70,13 @@ class PhotosDB:
from ._photosdb_process_scoreinfo import _process_scoreinfo
from ._photosdb_process_comments import _process_comments
def __init__(self, dbfile=None, verbose=None):
def __init__(self, dbfile=None, verbose=None, exiftool=None):
""" Create a new PhotosDB object.
Args:
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
Raises:
FileNotFoundError if dbfile is not a valid Photos library.
@@ -84,12 +85,12 @@ class PhotosDB:
# Check OS version
system = platform.system()
(_, major, _) = _get_os_version()
if system != "Darwin" or (major not in _TESTED_OS_VERSIONS):
(ver, major, _) = _get_os_version()
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
logging.warning(
f"WARNING: This module has only been tested with MacOS 10."
f"[{', '.join(_TESTED_OS_VERSIONS)}]: "
f"you have {system}, OS version: {major}"
f"WARNING: This module has only been tested with macOS versions "
f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: "
f"you have {system}, OS version: {ver}.{major}"
)
if verbose is None:
@@ -98,6 +99,11 @@ class PhotosDB:
raise TypeError("verbose must be callable")
self._verbose = verbose
# enable beta features
self._beta = False
self._exiftool_path = exiftool
# create a temporary directory
# tempfile.TemporaryDirectory gets cleaned up when the object does
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
@@ -531,14 +537,14 @@ class PhotosDB:
try:
dest_name = pathlib.Path(fname).name
dest_path = os.path.join(self._tempdir_name, dest_name)
copyfile(fname, dest_path)
FileUtil.copy(fname, dest_path)
# copy write-ahead log and shared memory files (-wal and -shm) files if they exist
if os.path.exists(f"{fname}-wal"):
copyfile(f"{fname}-wal", f"{dest_path}-wal")
FileUtil.copy(f"{fname}-wal", f"{dest_path}-wal")
if os.path.exists(f"{fname}-shm"):
copyfile(f"{fname}-shm", f"{dest_path}-shm")
FileUtil.copy(f"{fname}-shm", f"{dest_path}-shm")
except:
print("Error copying " + fname + " to " + dest_path, file=sys.stderr)
print(f"Error copying{fname} to {dest_path}", file=sys.stderr)
raise Exception
if _debug():
@@ -546,6 +552,33 @@ class PhotosDB:
return dest_path
# NOTE: This method seems to cause problems with applescript
# Bummer...would'be been nice to avoid copying the DB
# def _link_db_file(self, fname):
# """ links the sqlite database file to a temp file """
# """ returns the name of the temp file """
# """ If sqlite shared memory and write-ahead log files exist, those are copied too """
# # required because python's sqlite3 implementation can't read a locked file
# # _, suffix = os.path.splitext(fname)
# dest_name = dest_path = ""
# try:
# dest_name = pathlib.Path(fname).name
# dest_path = os.path.join(self._tempdir_name, dest_name)
# FileUtil.hardlink(fname, dest_path)
# # link write-ahead log and shared memory files (-wal and -shm) files if they exist
# if os.path.exists(f"{fname}-wal"):
# FileUtil.hardlink(f"{fname}-wal", f"{dest_path}-wal")
# if os.path.exists(f"{fname}-shm"):
# FileUtil.hardlink(f"{fname}-shm", f"{dest_path}-shm")
# except:
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
# raise Exception
# if _debug():
# logging.debug(dest_path)
# return dest_path
def _process_database4(self):
""" process the Photos database to extract info
works on Photos version <= 4.0 """
@@ -857,7 +890,10 @@ class PhotosDB:
RKMaster.width,
RKMaster.orientation,
RKMaster.fileSize,
RKVersion.subType
RKVersion.subType,
RKVersion.inTrashDate,
RKVersion.showInLibrary,
RKMaster.fileIsReference
FROM RKVersion, RKMaster
WHERE RKVersion.masterUuid = RKMaster.uuid"""
)
@@ -885,8 +921,11 @@ class PhotosDB:
RKMaster.width,
RKMaster.orientation,
RKMaster.originalFileSize,
RKVersion.subType
FROM RKVersion, RKMaster
RKVersion.subType,
RKVersion.inTrashDate,
RKVersion.showInLibrary,
RKMaster.fileIsReference
FROM RKVersion, RKMaster
WHERE RKVersion.masterUuid = RKMaster.uuid"""
)
@@ -932,6 +971,9 @@ class PhotosDB:
# 38 RKMaster.orientation,
# 39 RKMaster.originalFileSize
# 40 RKVersion.subType
# 41 RKVersion.inTrashDate
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
# 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)
for row in c:
uuid = row[0]
@@ -1106,7 +1148,14 @@ class PhotosDB:
)
# recently deleted items
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
self._dbphotos[uuid]["intrash"] = row[32] == 1
self._dbphotos[uuid]["trasheddate_timestamp"] = row[41]
try:
self._dbphotos[uuid]["trasheddate"] = datetime.fromtimestamp(
row[41] + TIME_DELTA
)
except (ValueError, TypeError):
self._dbphotos[uuid]["trasheddate"] = None
# height/width/orientation
self._dbphotos[uuid]["height"] = row[33]
@@ -1117,6 +1166,14 @@ class PhotosDB:
self._dbphotos[uuid]["original_orientation"] = row[38]
self._dbphotos[uuid]["original_filesize"] = row[39]
# visibility state
self._dbphotos[uuid]["visibility_state"] = row[42]
self._dbphotos[uuid]["visible"] = row[42] == 1
# file is reference (not copied into Photos library)
self._dbphotos[uuid]["isreference"] = row[43] == 1
self._dbphotos[uuid]["saved_asset_type"] = None # Photos 5+
# import session not yet handled for Photos 4
self._dbphotos[uuid]["import_session"] = None
self._dbphotos[uuid]["import_uuid"] = None
@@ -1810,7 +1867,10 @@ class PhotosDB:
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
{depth_state},
{asset_table}.ZADJUSTMENTTIMESTAMP
{asset_table}.ZADJUSTMENTTIMESTAMP,
{asset_table}.ZVISIBILITYSTATE,
{asset_table}.ZTRASHEDDATE,
{asset_table}.ZSAVEDASSETTYPE
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
ORDER BY {asset_table}.ZUUID """
@@ -1855,6 +1915,9 @@ class PhotosDB:
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
for row in c:
uuid = row[0]
@@ -1871,9 +1934,7 @@ class PhotosDB:
info["lastmodifieddate_timestamp"] = row[37]
try:
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
except ValueError:
info["lastmodifieddate"] = None
except TypeError:
except (ValueError, TypeError):
info["lastmodifieddate"] = None
info["imageTimeZoneOffsetSeconds"] = row[6]
@@ -2016,6 +2077,11 @@ class PhotosDB:
# recently deleted items
info["intrash"] = True if row[28] == 1 else False
info["trasheddate_timestamp"] = row[39]
try:
info["trasheddate"] = datetime.fromtimestamp(row[39] + TIME_DELTA)
except (ValueError, TypeError):
info["trasheddate"] = None
# height/width/orientation
info["height"] = row[29]
@@ -2026,6 +2092,19 @@ class PhotosDB:
info["original_orientation"] = row[34]
info["original_filesize"] = row[35]
# visibility state, visible (True) if 0, otherwise not visible (False)
# only values I've seen are 0 for visible, 2 for not-visible
info["visibility_state"] = row[38]
info["visible"] = row[38] == 0
# ZSAVEDASSETTYPE Values:
# 3: imported by copying to Photos library
# 4: shared iCloud photo
# 6: imported by iCloud (e.g. from iPhone)
# 10: referenced file (not copied to Photos library)
info["saved_asset_type"] = row[40]
info["isreference"] = row[40] == 10
# initialize import session info which will be filled in later
# not every photo has an import session so initialize all records now
info["import_session"] = None

File diff suppressed because it is too large Load Diff

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,5 +1,3 @@
<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
<%def name="photoshop_sidecar_for_extension(extension)">
% if extension is None:
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
@@ -10,29 +8,44 @@
<%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>
@@ -46,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>
@@ -58,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>
@@ -81,41 +94,117 @@
<%def name="gps_info(latitude, longitude)">
% if latitude is not None and longitude is not None:
<exif:GPSLongitudeRef>${"E" if longitude >= 0 else "W"}</exif:GPSLongitudeRef>
<exif:GPSLongitude>${abs(longitude)}</exif:GPSLongitude>
<exif:GPSLatitude>${abs(latitude)}</exif:GPSLatitude>
<exif:GPSLatitudeRef>${"N" if latitude >= 0 else "S"}</exif:GPSLatitudeRef>
<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>
<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/">
${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(*photo.location)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<%def name="orientation(orientation)">
% if orientation is not None:
<tiff:Orientation>${orientation}</tiff:Orientation>
% 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 ${version}">
<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(*photo.location)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
${orientation(photo.orientation)}
</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"?>

View File

@@ -0,0 +1,210 @@
<%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>
<rdf:Alt>
<rdf:li xml:lang='x-default'/>
</rdf:Alt>
</dc:description>
% else:
<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>
<rdf:Alt>
<rdf:li xml:lang='x-default'/>
</rdf:Alt>
</dc:title>
% else:
<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:
<dc:subject>
<rdf:Bag>
% for subj in subject:
<rdf:li>${subj | x}</rdf:li>
% endfor
</rdf:Bag>
</dc:subject>
% endif
</%def>
<%def name="dc_datecreated(date)">
% if date is not None:
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
% endif
</%def>
<%def name="iptc_personinimage(persons)">
% if persons:
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
% for person in persons:
<rdf:li>${person | x}</rdf:li>
% endfor
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
% endif
</%def>
<%def name="dk_tagslist(keywords)">
% if keywords:
<digiKam:TagsList>
<rdf:Seq>
% for keyword in keywords:
<rdf:li>${keyword | x}</rdf:li>
% endfor
</rdf:Seq>
</digiKam:TagsList>
% endif
</%def>
<%def name="adobe_createdate(date)">
% if date is not None:
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
% endif
</%def>
<%def name="adobe_modifydate(date)">
% if date is not None:
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
% endif
</%def>
<%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="orientation(orientation)">
% if orientation is not None:
<tiff:Orientation>${orientation}</tiff:Orientation>
% 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 ${version}">
<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(*photo.location)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
${orientation(photo.orientation)}
</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"?>

View File

@@ -1,5 +1,6 @@
import fnmatch
import glob
import inspect
import logging
import os
import os.path
@@ -9,7 +10,6 @@ import re
import sqlite3
import subprocess
import sys
import tempfile
import unicodedata
import urllib.parse
from plistlib import load as plistload
@@ -17,10 +17,8 @@ from plistlib import load as plistload
import CoreFoundation
import CoreServices
import objc
from Foundation import *
from ._constants import UNICODE_FORMAT
from .fileutil import FileUtil
_DEBUG = False
@@ -57,13 +55,23 @@ def _debug():
""" returns True if debugging turned on (via _set_debug), otherwise, false """
return _DEBUG
def noop(*args, **kwargs):
""" do nothing (no operation) """
pass
def lineno(filename):
""" Returns string with filename and current line number in caller as '(filename): line_num'
Will trim filename to just the name, dropping path, if any. """
line = inspect.currentframe().f_back.f_lineno
filename = pathlib.Path(filename).name
return f"{filename}: {line}"
def _get_os_version():
# returns tuple containing OS version
# e.g. 10.13.6 = (10, 13, 6)
# returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".")
if len(version) == 2:
(ver, major) = version
@@ -200,7 +208,7 @@ def get_last_library_path():
# pylint: disable=no-member
# pylint: disable=undefined-variable
photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData(
kCFAllocatorDefault, photosurlref, 0, None, None, None, None
CoreFoundation.kCFAllocatorDefault, photosurlref, 0, None, None, None, None
)
# the CFURLRef we got is a sruct that python treats as an array
@@ -259,10 +267,10 @@ def get_preferred_uti_extension(uti):
returns: preferred extension as str """
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
return CoreServices.UTTypeCopyPreferredTagWithClass(
uti, CoreServices.kUTTagClassFilenameExtension
)
with objc.autorelease_pool():
return CoreServices.UTTypeCopyPreferredTagWithClass(
uti, CoreServices.kUTTagClassFilenameExtension
)
def findfiles(pattern, path_):
@@ -361,9 +369,35 @@ def _db_is_locked(dbname):
def normalize_unicode(value):
""" normalize unicode data """
if value is not None:
if not isinstance(value, str):
raise ValueError("value must be str")
return unicodedata.normalize(UNICODE_FORMAT, value)
else:
if value is None:
return None
if not isinstance(value, str):
raise ValueError("value must be str")
return unicodedata.normalize(UNICODE_FORMAT, value)
def increment_filename(filepath):
""" Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
add (1), (2), etc. until a non-existing filename is found.
Args:
filepath: str; full path, including file name
Returns:
new filepath (or same if not incremented)
Note: This obviously is subject to race condition so using with caution.
"""
dest = pathlib.Path(str(filepath))
count = 1
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
dest_new = dest.stem
while dest_new.lower() in dest_files:
dest_new = f"{dest.stem} ({count})"
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest)

View File

@@ -42,11 +42,13 @@ mccabe==0.6.1
modulegraph==0.18
more-itertools==7.2.0
multidict==4.7.6
osxmetadata>=0.99.11
packaging==19.0
parso==0.6.2
pathspec==0.7.0
pathvalidate==2.2.1
pexpect==4.8.0
photoscript==0.1.0
pickleshare==0.7.5
Pillow==7.2.0
pkginfo==1.5.0.1

View File

@@ -3,7 +3,7 @@
#
# setup.py script for osxphotos
#
# Copyright (c) 2019, 2020 Rhet Turnbull, rturnbull+git@gmail.com
# Copyright (c) 2019, 2020, 2021 Rhet Turnbull, rturnbull+git@gmail.com
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person
@@ -45,15 +45,15 @@ with open(
exec(f.read(), about)
# read README.md into long_description
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
with open(os.path.join(this_directory, "README.rst"), encoding="utf-8") as f:
about["long_description"] = f.read()
setup(
name="osxphotos",
version=about["__version__"],
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
description="Export photos from Apple's macOS Photos app and query the Photos library database to access metadata about images.",
long_description=about["long_description"],
long_description_content_type="text/markdown",
long_description_content_type="text/x-rst",
author="Rhet Turnbull",
author_email="rturnbull+git@gmail.com",
url="https://github.com/RhetTbull/",
@@ -68,6 +68,8 @@ setup(
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
@@ -79,6 +81,9 @@ setup(
"pathvalidate==2.2.1",
"dataclasses==0.7;python_version<'3.7'",
"wurlitzer>=2.0.1",
"photoscript>=0.1.0",
"toml>=0.10.0",
"osxmetadata>=0.99.13",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
include_package_data=True,

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-09T16:14:42Z</date>
<date>2020-11-01T02:34:49Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-10-10T05:21:03Z</date>
<date>2020-11-01T02:34:49Z</date>
</dict>
</plist>

View File

@@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-10-04T23:43:17Z</date>
<date>2020-11-01T02:34:46Z</date>
</dict>
</plist>

View File

@@ -24,7 +24,7 @@
<key>SnapshotCompletedDate</key>
<date>2019-07-27T13:16:43Z</date>
<key>SnapshotLastValidated</key>
<date>2020-10-10T05:22:36Z</date>
<date>2020-11-01T02:34:46Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>1797</integer>
<integer>55247</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:42Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundJobSearch</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:41Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-10-17T23:45:33Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-17T23:45:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-10-17T23:45:26Z</date>
<date>2020-12-16T05:41:44Z</date>
<key>SiriPortraitDonation</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>440</integer>
<integer>400</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

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