Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ebf995833 | ||
|
|
538bac7ade | ||
|
|
32806c8459 | ||
|
|
cfabd0dbea | ||
|
|
a23259948c | ||
|
|
1212fad4ad | ||
|
|
567abe3311 | ||
|
|
5a832181f7 | ||
|
|
4da57a1cee | ||
|
|
1fd0f96b14 | ||
|
|
e98c3fe429 | ||
|
|
d77e9747cd | ||
|
|
43d28e78f3 | ||
|
|
00bc50490e | ||
|
|
f8743c33bd | ||
|
|
937da9e617 | ||
|
|
435868a0a7 | ||
|
|
d9802247d9 | ||
|
|
f39a92a352 | ||
|
|
40dc7d32f2 | ||
|
|
4cd6c8f617 | ||
|
|
0004250e74 | ||
|
|
868ee7737b | ||
|
|
5387f8e2f9 | ||
|
|
73b499f405 | ||
|
|
06fa1edcae | ||
|
|
cf2615da62 | ||
|
|
4ba1982d74 | ||
|
|
abd10b73e8 | ||
|
|
7cd7b51598 | ||
|
|
801dc62c4b | ||
|
|
72f034ef85 | ||
|
|
cb993f2e5e | ||
|
|
2271d89355 | ||
|
|
62d096b5a1 | ||
|
|
5c7a0c3a24 | ||
|
|
ec727cc556 |
137
CHANGELOG.md
137
CHANGELOG.md
@@ -4,6 +4,83 @@ 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).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.29.24](https://github.com/RhetTbull/osxphotos/compare/v0.29.23...v0.29.24)
|
||||||
|
|
||||||
|
> 21 June 2020
|
||||||
|
|
||||||
|
- Refactored album code in photosdb to fix issue #169 [`cfabd0d`](https://github.com/RhetTbull/osxphotos/commit/cfabd0dbead62c8ab6a774899239e5da5bfe1203)
|
||||||
|
|
||||||
|
#### [v0.29.23](https://github.com/RhetTbull/osxphotos/compare/v0.29.22...v0.29.23)
|
||||||
|
|
||||||
|
> 20 June 2020
|
||||||
|
|
||||||
|
- Fixed PhotoInfo.albums, album_info for issue #169 [`1212fad`](https://github.com/RhetTbull/osxphotos/commit/1212fad4adde0b4c6b2887392eed829d8d96d61d)
|
||||||
|
|
||||||
|
#### [v0.29.22](https://github.com/RhetTbull/osxphotos/compare/v0.29.19...v0.29.22)
|
||||||
|
|
||||||
|
> 19 June 2020
|
||||||
|
|
||||||
|
- Don't raise KeyError when SystemLibraryPath is absent [`#168`](https://github.com/RhetTbull/osxphotos/pull/168)
|
||||||
|
- Added check for export db in directory branch, closes #164 [`#164`](https://github.com/RhetTbull/osxphotos/issues/164)
|
||||||
|
- Added OSXPhotosDB.get_db_connection() [`43d28e7`](https://github.com/RhetTbull/osxphotos/commit/43d28e78f394fa33f8d88f64b56b7dc7258cd454)
|
||||||
|
- Added show() to photos_repl.py [`e98c3fe`](https://github.com/RhetTbull/osxphotos/commit/e98c3fe42912ac16d13675bf14154981089d41ea)
|
||||||
|
- Fixed get_last_library_path and get_system_library_path to not raise KeyError [`5a83218`](https://github.com/RhetTbull/osxphotos/commit/5a832181f73e082927c80864f2063e554906b06b)
|
||||||
|
- Don't raise KeyError when SystemLibraryPath is absent [`1fd0f96`](https://github.com/RhetTbull/osxphotos/commit/1fd0f96b14f0bc38e47bddb4cae12e19406324fb)
|
||||||
|
|
||||||
|
#### [v0.29.19](https://github.com/RhetTbull/osxphotos/compare/v0.29.18...v0.29.19)
|
||||||
|
|
||||||
|
> 14 June 2020
|
||||||
|
|
||||||
|
- Added computed aesthetic scores, closes #141, closes #122 [`#141`](https://github.com/RhetTbull/osxphotos/issues/141) [`#122`](https://github.com/RhetTbull/osxphotos/issues/122)
|
||||||
|
|
||||||
|
#### [v0.29.18](https://github.com/RhetTbull/osxphotos/compare/v0.29.17...v0.29.18)
|
||||||
|
|
||||||
|
> 14 June 2020
|
||||||
|
|
||||||
|
- Added --label to CLI, closes #157 [`#157`](https://github.com/RhetTbull/osxphotos/issues/157)
|
||||||
|
|
||||||
|
#### [v0.29.17](https://github.com/RhetTbull/osxphotos/compare/v0.29.16...v0.29.17)
|
||||||
|
|
||||||
|
> 13 June 2020
|
||||||
|
|
||||||
|
- Extende --ignore-case to --person, --keyword, --album, closes #162 [`#162`](https://github.com/RhetTbull/osxphotos/issues/162)
|
||||||
|
- Updated README.md to document template system [`0004250`](https://github.com/RhetTbull/osxphotos/commit/0004250e74eacc19f7986742712225116530a67e)
|
||||||
|
|
||||||
|
#### [v0.29.16](https://github.com/RhetTbull/osxphotos/compare/v0.29.14...v0.29.16)
|
||||||
|
|
||||||
|
> 13 June 2020
|
||||||
|
|
||||||
|
- Added hour, min, sec, strftime templates, closes #158 [`#158`](https://github.com/RhetTbull/osxphotos/issues/158)
|
||||||
|
- Added hour, min, sec to template system, issue #158 [`5387f8e`](https://github.com/RhetTbull/osxphotos/commit/5387f8e2f970ff7fa1967ccad87b45a4f7e50d32)
|
||||||
|
|
||||||
|
#### [v0.29.14](https://github.com/RhetTbull/osxphotos/compare/v0.29.13...v0.29.14)
|
||||||
|
|
||||||
|
> 13 June 2020
|
||||||
|
|
||||||
|
- Updated DatetimeFormatter to include hour/min/sec [`cf2615d`](https://github.com/RhetTbull/osxphotos/commit/cf2615da62801f1fbde61c7905431963e121e2e9)
|
||||||
|
- Added test for issue #156 [`4ba1982`](https://github.com/RhetTbull/osxphotos/commit/4ba1982d745f0d532ead090177051d928465ed03)
|
||||||
|
- Bug fix for issue #136 [`06fa1ed`](https://github.com/RhetTbull/osxphotos/commit/06fa1edcae7139b543e17ec63810c37c18cc2780)
|
||||||
|
|
||||||
|
#### [v0.29.13](https://github.com/RhetTbull/osxphotos/compare/v0.29.12...v0.29.13)
|
||||||
|
|
||||||
|
> 7 June 2020
|
||||||
|
|
||||||
|
- Added hidden debug-dump command to CLI [`7cd7b51`](https://github.com/RhetTbull/osxphotos/commit/7cd7b5159845fce15d50a7bfc0ac50d122bee527)
|
||||||
|
|
||||||
|
#### [v0.29.12](https://github.com/RhetTbull/osxphotos/compare/v0.29.9...v0.29.12)
|
||||||
|
|
||||||
|
> 7 June 2020
|
||||||
|
|
||||||
|
- Fix for bug in handling of deleted albums to address issue #156 [`72f034e`](https://github.com/RhetTbull/osxphotos/commit/72f034ef85010544a158d8301b898b5d0d865b05)
|
||||||
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`cb993f2`](https://github.com/RhetTbull/osxphotos/commit/cb993f2e5e2df7e0a15b3b2fdb92b65a8de56974)
|
||||||
|
- Refactoring with sourceryAI [`5c7a0c3`](https://github.com/RhetTbull/osxphotos/commit/5c7a0c3a246cd5fec329b4fd4979d2b77352f916)
|
||||||
|
|
||||||
|
#### [v0.29.9](https://github.com/RhetTbull/osxphotos/compare/v0.29.8...v0.29.9)
|
||||||
|
|
||||||
|
> 31 May 2020
|
||||||
|
|
||||||
|
- Added --filename to CLI, closes #89 [`#89`](https://github.com/RhetTbull/osxphotos/issues/89)
|
||||||
|
|
||||||
#### [v0.29.8](https://github.com/RhetTbull/osxphotos/compare/v0.29.5...v0.29.8)
|
#### [v0.29.8](https://github.com/RhetTbull/osxphotos/compare/v0.29.5...v0.29.8)
|
||||||
|
|
||||||
> 31 May 2020
|
> 31 May 2020
|
||||||
@@ -24,7 +101,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Added --dry-run option to CLI export, closes #91 [`#91`](https://github.com/RhetTbull/osxphotos/issues/91)
|
- Added --dry-run option to CLI export, closes #91 [`#91`](https://github.com/RhetTbull/osxphotos/issues/91)
|
||||||
- added created.dd and modified.dd to template system, closes #135 [`#135`](https://github.com/RhetTbull/osxphotos/issues/135)
|
- added created.dd and modified.dd to template system, closes #135 [`#135`](https://github.com/RhetTbull/osxphotos/issues/135)
|
||||||
- Catch exception in folder processing to address #148 [`46fdc94`](https://github.com/RhetTbull/osxphotos/commit/46fdc94398c80b157048649434c7312074ce5c58)
|
- Catch exception in folder processing to address #148 [`46fdc94`](https://github.com/RhetTbull/osxphotos/commit/46fdc94398c80b157048649434c7312074ce5c58)
|
||||||
- Updated CHANGELOG.md [`af750dd`](https://github.com/RhetTbull/osxphotos/commit/af750dd2e392be1a7163cf32497526405665ea70)
|
- added created.dow (day of week) to template [`8df6d2c`](https://github.com/RhetTbull/osxphotos/commit/8df6d2c707caf4eb35696888282365a128b69569)
|
||||||
- Added test for DateTimeFormatter.dow [`09c7d18`](https://github.com/RhetTbull/osxphotos/commit/09c7d18901b61669d8b9242babd82eba6987c89a)
|
- Added test for DateTimeFormatter.dow [`09c7d18`](https://github.com/RhetTbull/osxphotos/commit/09c7d18901b61669d8b9242babd82eba6987c89a)
|
||||||
|
|
||||||
#### [v0.29.2](https://github.com/RhetTbull/osxphotos/compare/v0.29.1...v0.29.2)
|
#### [v0.29.2](https://github.com/RhetTbull/osxphotos/compare/v0.29.1...v0.29.2)
|
||||||
@@ -38,7 +115,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 23 May 2020
|
> 23 May 2020
|
||||||
|
|
||||||
- Catch illegal timestamp value [`#146`](https://github.com/RhetTbull/osxphotos/pull/146)
|
- Catch illegal timestamp value [`#146`](https://github.com/RhetTbull/osxphotos/pull/146)
|
||||||
- Updated CHANGELOG.md [`1450b3c`](https://github.com/RhetTbull/osxphotos/commit/1450b3ccace326fe1c0ed810a1b40e781709acb3)
|
- Catch illegal timestamp value [`441de71`](https://github.com/RhetTbull/osxphotos/commit/441de711dc664b244d599c81e3dd1bcd9b2e55a0)
|
||||||
|
|
||||||
#### [v0.29.0](https://github.com/RhetTbull/osxphotos/compare/v0.28.19...v0.29.0)
|
#### [v0.29.0](https://github.com/RhetTbull/osxphotos/compare/v0.28.19...v0.29.0)
|
||||||
|
|
||||||
@@ -48,8 +125,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Added --update to CLI export; reference issue #100 [`b1171e9`](https://github.com/RhetTbull/osxphotos/commit/b1171e96cc06362555725995bb311317eb163e49)
|
- Added --update to CLI export; reference issue #100 [`b1171e9`](https://github.com/RhetTbull/osxphotos/commit/b1171e96cc06362555725995bb311317eb163e49)
|
||||||
- Added as_dict to PlaceInfo [`8c4fe40`](https://github.com/RhetTbull/osxphotos/commit/8c4fe40aa6850f166e526cffaa088550884399af)
|
- Added as_dict to PlaceInfo [`8c4fe40`](https://github.com/RhetTbull/osxphotos/commit/8c4fe40aa6850f166e526cffaa088550884399af)
|
||||||
- Updated README.md [`11d368a`](https://github.com/RhetTbull/osxphotos/commit/11d368a69cbe67e909e64b020f0334fc09dd3ac4)
|
- Updated README.md [`11d368a`](https://github.com/RhetTbull/osxphotos/commit/11d368a69cbe67e909e64b020f0334fc09dd3ac4)
|
||||||
- Updated CHANGELOG.md [`cafa483`](https://github.com/RhetTbull/osxphotos/commit/cafa483cfc228c651a03d3361d6d48a35deab1e8)
|
|
||||||
- version bump [`c06c230`](https://github.com/RhetTbull/osxphotos/commit/c06c230a469754691d11fff1034fb02daeeba649)
|
- version bump [`c06c230`](https://github.com/RhetTbull/osxphotos/commit/c06c230a469754691d11fff1034fb02daeeba649)
|
||||||
|
- Test library update [`f416418`](https://github.com/RhetTbull/osxphotos/commit/f416418546a12bc6c1bda13f6b712758584d06dc)
|
||||||
|
|
||||||
#### [v0.28.19](https://github.com/RhetTbull/osxphotos/compare/v0.28.18...v0.28.19)
|
#### [v0.28.19](https://github.com/RhetTbull/osxphotos/compare/v0.28.18...v0.28.19)
|
||||||
|
|
||||||
@@ -58,16 +135,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Added label and label_normalized to template system, closes #130 [`#130`](https://github.com/RhetTbull/osxphotos/issues/130)
|
- Added label and label_normalized to template system, closes #130 [`#130`](https://github.com/RhetTbull/osxphotos/issues/130)
|
||||||
- Revert "test library updates" [`48e9c32`](https://github.com/RhetTbull/osxphotos/commit/48e9c32add549e66c3ef8c65f8821f5033b55b11)
|
- Revert "test library updates" [`48e9c32`](https://github.com/RhetTbull/osxphotos/commit/48e9c32add549e66c3ef8c65f8821f5033b55b11)
|
||||||
- test library updates [`d125854`](https://github.com/RhetTbull/osxphotos/commit/d125854f2a04e37747af3e0796370a565c1c9bd0)
|
- test library updates [`d125854`](https://github.com/RhetTbull/osxphotos/commit/d125854f2a04e37747af3e0796370a565c1c9bd0)
|
||||||
- Updated CHANGELOG.md [`e228cfa`](https://github.com/RhetTbull/osxphotos/commit/e228cfab746055c8d6df428aebe0ed001fb6d4d0)
|
|
||||||
- version bump [`bd9d5a2`](https://github.com/RhetTbull/osxphotos/commit/bd9d5a26f3bfcbb33896a139fa86cdab46768103)
|
- version bump [`bd9d5a2`](https://github.com/RhetTbull/osxphotos/commit/bd9d5a26f3bfcbb33896a139fa86cdab46768103)
|
||||||
- Update README.md [`85760dc`](https://github.com/RhetTbull/osxphotos/commit/85760dc4fe2274d826ed80494fd4e66866398609)
|
- Update README.md [`85760dc`](https://github.com/RhetTbull/osxphotos/commit/85760dc4fe2274d826ed80494fd4e66866398609)
|
||||||
|
- Update README.md [`be07f90`](https://github.com/RhetTbull/osxphotos/commit/be07f90e5a8179e452730ea654e4c9627b1f6ebc)
|
||||||
|
|
||||||
#### [v0.28.18](https://github.com/RhetTbull/osxphotos/compare/v0.28.17...v0.28.18)
|
#### [v0.28.18](https://github.com/RhetTbull/osxphotos/compare/v0.28.17...v0.28.18)
|
||||||
|
|
||||||
> 14 May 2020
|
> 14 May 2020
|
||||||
|
|
||||||
- Implemented PhotoInfo.exiftool [`a80dee4`](https://github.com/RhetTbull/osxphotos/commit/a80dee401c7eb959f6ad6d93a3272657ed28f521)
|
- Implemented PhotoInfo.exiftool [`a80dee4`](https://github.com/RhetTbull/osxphotos/commit/a80dee401c7eb959f6ad6d93a3272657ed28f521)
|
||||||
- Updated CHANGELOG.md [`e67fce2`](https://github.com/RhetTbull/osxphotos/commit/e67fce28714cf4065b64202bb3b149ba5bec5be4)
|
|
||||||
|
|
||||||
#### [v0.28.17](https://github.com/RhetTbull/osxphotos/compare/v0.28.15...v0.28.17)
|
#### [v0.28.17](https://github.com/RhetTbull/osxphotos/compare/v0.28.15...v0.28.17)
|
||||||
|
|
||||||
@@ -84,8 +160,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- added --export-as-hardlink option [`#126`](https://github.com/RhetTbull/osxphotos/pull/126)
|
- added --export-as-hardlink option [`#126`](https://github.com/RhetTbull/osxphotos/pull/126)
|
||||||
- Added test for folder_names on 10.15.4, closes #119 [`#119`](https://github.com/RhetTbull/osxphotos/issues/119)
|
- Added test for folder_names on 10.15.4, closes #119 [`#119`](https://github.com/RhetTbull/osxphotos/issues/119)
|
||||||
- Refactored photosdb and photoinfo to add SearchInfo and labels [`98b3f63`](https://github.com/RhetTbull/osxphotos/commit/98b3f63a92aa2105f8fa97af992fc6fe2d78b973)
|
- Refactored photosdb and photoinfo to add SearchInfo and labels [`98b3f63`](https://github.com/RhetTbull/osxphotos/commit/98b3f63a92aa2105f8fa97af992fc6fe2d78b973)
|
||||||
|
- added --export-as-hardlink option [`5eb0876`](https://github.com/RhetTbull/osxphotos/commit/5eb0876e331beb020431bb037dee75fb7ae61c85)
|
||||||
- Added additional test for --export-as-hardlink [`57315d4`](https://github.com/RhetTbull/osxphotos/commit/57315d44497fde977956f76f667470208f11aa2d)
|
- Added additional test for --export-as-hardlink [`57315d4`](https://github.com/RhetTbull/osxphotos/commit/57315d44497fde977956f76f667470208f11aa2d)
|
||||||
- added CHANGELOG.md [`00e1661`](https://github.com/RhetTbull/osxphotos/commit/00e16611fc86c05fb090d036084db9eb42444071)
|
|
||||||
- Updated a couple of tests to use pytest-mock [`397db0d`](https://github.com/RhetTbull/osxphotos/commit/397db0d72fb218669a9ecbff134fa9b392a14661)
|
- Updated a couple of tests to use pytest-mock [`397db0d`](https://github.com/RhetTbull/osxphotos/commit/397db0d72fb218669a9ecbff134fa9b392a14661)
|
||||||
- added test for export using hardlinks, fixed a test that failed if users locale settings were different to en_US [`b0ec6c6`](https://github.com/RhetTbull/osxphotos/commit/b0ec6c6b36d8cfe05723d47b210d9d7c5aabdfe5)
|
- added test for export using hardlinks, fixed a test that failed if users locale settings were different to en_US [`b0ec6c6`](https://github.com/RhetTbull/osxphotos/commit/b0ec6c6b36d8cfe05723d47b210d9d7c5aabdfe5)
|
||||||
|
|
||||||
@@ -109,7 +185,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 28 April 2020
|
> 28 April 2020
|
||||||
|
|
||||||
- Fixed implementation of use_albums_as_keywords and use_persons_as_keywords, closes #115 [`#115`](https://github.com/RhetTbull/osxphotos/issues/115)
|
- Fixed implementation of use_albums_as_keywords and use_persons_as_keywords, closes #115 [`#115`](https://github.com/RhetTbull/osxphotos/issues/115)
|
||||||
- Updated CHANGELOG.md [`072a8d7`](https://github.com/RhetTbull/osxphotos/commit/072a8d795e5e15fa8ca8d8872aecf4cddd7837f7)
|
|
||||||
- Update README.md [`5cc98c3`](https://github.com/RhetTbull/osxphotos/commit/5cc98c338bcc19fd05bf293eb3afe24c07c8b380)
|
- Update README.md [`5cc98c3`](https://github.com/RhetTbull/osxphotos/commit/5cc98c338bcc19fd05bf293eb3afe24c07c8b380)
|
||||||
- Updated README.md [`a800711`](https://github.com/RhetTbull/osxphotos/commit/a80071111f810a1d7d6e2d735839e85499091ea4)
|
- Updated README.md [`a800711`](https://github.com/RhetTbull/osxphotos/commit/a80071111f810a1d7d6e2d735839e85499091ea4)
|
||||||
- Update README.md [`1c9d4f2`](https://github.com/RhetTbull/osxphotos/commit/1c9d4f282beea2ac12273c8d0f9453bad1255c2c)
|
- Update README.md [`1c9d4f2`](https://github.com/RhetTbull/osxphotos/commit/1c9d4f282beea2ac12273c8d0f9453bad1255c2c)
|
||||||
@@ -120,7 +195,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Added --album-keyword and --person-keyword to CLI, closes #61 [`#61`](https://github.com/RhetTbull/osxphotos/issues/61)
|
- Added --album-keyword and --person-keyword to CLI, closes #61 [`#61`](https://github.com/RhetTbull/osxphotos/issues/61)
|
||||||
- Updated test libraries [`54d5d4b`](https://github.com/RhetTbull/osxphotos/commit/54d5d4b7ba99204f58e723231309ab6e306be28c)
|
- Updated test libraries [`54d5d4b`](https://github.com/RhetTbull/osxphotos/commit/54d5d4b7ba99204f58e723231309ab6e306be28c)
|
||||||
- Updated CHANGELOG.md [`38137a1`](https://github.com/RhetTbull/osxphotos/commit/38137a1351cdb7ab72393ea03828933dac0b76b0)
|
|
||||||
- Updated tests/README.md [`56a0006`](https://github.com/RhetTbull/osxphotos/commit/56a000609f2f08d0f8800fec49cada2980c3bb9d)
|
- Updated tests/README.md [`56a0006`](https://github.com/RhetTbull/osxphotos/commit/56a000609f2f08d0f8800fec49cada2980c3bb9d)
|
||||||
|
|
||||||
#### [v0.28.6](https://github.com/RhetTbull/osxphotos/compare/v0.28.5...v0.28.6)
|
#### [v0.28.6](https://github.com/RhetTbull/osxphotos/compare/v0.28.5...v0.28.6)
|
||||||
@@ -128,7 +202,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 26 April 2020
|
> 26 April 2020
|
||||||
|
|
||||||
- Fixed locale bug in templates, closes #113 [`#113`](https://github.com/RhetTbull/osxphotos/issues/113)
|
- Fixed locale bug in templates, closes #113 [`#113`](https://github.com/RhetTbull/osxphotos/issues/113)
|
||||||
- Updated CHANGELOG.md [`81d4e39`](https://github.com/RhetTbull/osxphotos/commit/81d4e392c39f0fe6f967a447c7d0c970bf224032)
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`4b29a2e`](https://github.com/RhetTbull/osxphotos/commit/4b29a2e05fd1dac821d80781ae01a148d3d9c523)
|
||||||
- Updated test to avoid issue with GitHub workflow [`9be0f84`](https://github.com/RhetTbull/osxphotos/commit/9be0f849b73061d053d30274ff3295b79c88f0b6)
|
- Updated test to avoid issue with GitHub workflow [`9be0f84`](https://github.com/RhetTbull/osxphotos/commit/9be0f849b73061d053d30274ff3295b79c88f0b6)
|
||||||
- Update pythonpackage.yml to remove older pythons [`ccb5f25`](https://github.com/RhetTbull/osxphotos/commit/ccb5f252d14e9335ae04a2e338a6d527b80c9a93)
|
- Update pythonpackage.yml to remove older pythons [`ccb5f25`](https://github.com/RhetTbull/osxphotos/commit/ccb5f252d14e9335ae04a2e338a6d527b80c9a93)
|
||||||
|
|
||||||
@@ -142,7 +216,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Updated setup.py to resolve issue with bpylist2 on python < 3.8 [`8e4b88a`](https://github.com/RhetTbull/osxphotos/commit/8e4b88ad1fc18438f941e045bfc8aeac878914f9)
|
- Updated setup.py to resolve issue with bpylist2 on python < 3.8 [`8e4b88a`](https://github.com/RhetTbull/osxphotos/commit/8e4b88ad1fc18438f941e045bfc8aeac878914f9)
|
||||||
- Added cli.py for use with pyinstaller [`cf28cb6`](https://github.com/RhetTbull/osxphotos/commit/cf28cb6452de17f2ef8d80435386e8d5a1aabd34)
|
- Added cli.py for use with pyinstaller [`cf28cb6`](https://github.com/RhetTbull/osxphotos/commit/cf28cb6452de17f2ef8d80435386e8d5a1aabd34)
|
||||||
- added raw_is_original handling [`a337e79`](https://github.com/RhetTbull/osxphotos/commit/a337e79e13802b4824c2f088ce9db1c027d6f3c5)
|
- added raw_is_original handling [`a337e79`](https://github.com/RhetTbull/osxphotos/commit/a337e79e13802b4824c2f088ce9db1c027d6f3c5)
|
||||||
- Updated CHANGELOG.md [`22f1e8f`](https://github.com/RhetTbull/osxphotos/commit/22f1e8f2a6478e0576f6bff53e348aad8680ae69)
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`1c8eb76`](https://github.com/RhetTbull/osxphotos/commit/1c8eb764f53c3cc8b541667c858e462793ad8d1f)
|
||||||
|
|
||||||
#### [0.28.2](https://github.com/RhetTbull/osxphotos/compare/v0.28.1...0.28.2)
|
#### [0.28.2](https://github.com/RhetTbull/osxphotos/compare/v0.28.1...0.28.2)
|
||||||
|
|
||||||
@@ -150,7 +224,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Added folder support for Photos <= 4, closes #93 [`#93`](https://github.com/RhetTbull/osxphotos/issues/93)
|
- Added folder support for Photos <= 4, closes #93 [`#93`](https://github.com/RhetTbull/osxphotos/issues/93)
|
||||||
- cleaned up SQL statements in _process_database4 [`6f28171`](https://github.com/RhetTbull/osxphotos/commit/6f281711e2001a63ffad076d7b9835272d5d09da)
|
- cleaned up SQL statements in _process_database4 [`6f28171`](https://github.com/RhetTbull/osxphotos/commit/6f281711e2001a63ffad076d7b9835272d5d09da)
|
||||||
- Updated CHANGELOG.md [`1fa9583`](https://github.com/RhetTbull/osxphotos/commit/1fa9583ea689d54d2613a064f1ade25bcdfbf043)
|
|
||||||
- Fixed suffix check on export to be case insensitive [`4b30b3b`](https://github.com/RhetTbull/osxphotos/commit/4b30b3b4260e2c7409e18825e5b626efe646db16)
|
- Fixed suffix check on export to be case insensitive [`4b30b3b`](https://github.com/RhetTbull/osxphotos/commit/4b30b3b4260e2c7409e18825e5b626efe646db16)
|
||||||
- test library update [`3bac106`](https://github.com/RhetTbull/osxphotos/commit/3bac106eb7a180e9e39643a89087d92bf2a437d0)
|
- test library update [`3bac106`](https://github.com/RhetTbull/osxphotos/commit/3bac106eb7a180e9e39643a89087d92bf2a437d0)
|
||||||
|
|
||||||
@@ -168,7 +241,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Added {folder_album} to template and --folder to CLI [`b7c7b9f`](https://github.com/RhetTbull/osxphotos/commit/b7c7b9f0664e69c743bdd8a228ad2936cf6b7600)
|
- Added {folder_album} to template and --folder to CLI [`b7c7b9f`](https://github.com/RhetTbull/osxphotos/commit/b7c7b9f0664e69c743bdd8a228ad2936cf6b7600)
|
||||||
- Test library update [`21e7020`](https://github.com/RhetTbull/osxphotos/commit/21e7020fec406b0f3926d7adc8a1451bfe77e75a)
|
- Test library update [`21e7020`](https://github.com/RhetTbull/osxphotos/commit/21e7020fec406b0f3926d7adc8a1451bfe77e75a)
|
||||||
- Updated CHANGELOG.md [`952741d`](https://github.com/RhetTbull/osxphotos/commit/952741d488d2fbbaf8a0c1d3781ad7c4205c068f)
|
|
||||||
|
|
||||||
#### [v0.27.3](https://github.com/RhetTbull/osxphotos/compare/v0.27.1...v0.27.3)
|
#### [v0.27.3](https://github.com/RhetTbull/osxphotos/compare/v0.27.1...v0.27.3)
|
||||||
|
|
||||||
@@ -182,7 +254,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 12 April 2020
|
> 12 April 2020
|
||||||
|
|
||||||
- Changed AlbumInfo and FolderInfo interface to maintain backwards compatibility with PhotosDB.albums [`e09f0b4`](https://github.com/RhetTbull/osxphotos/commit/e09f0b40f1671d70ee399cdc519492b04fac8adc)
|
- Changed AlbumInfo and FolderInfo interface to maintain backwards compatibility with PhotosDB.albums [`e09f0b4`](https://github.com/RhetTbull/osxphotos/commit/e09f0b40f1671d70ee399cdc519492b04fac8adc)
|
||||||
- Updated CHANGELOG.md [`b749681`](https://github.com/RhetTbull/osxphotos/commit/b749681c6d2545eacf653ab1b2a5d1384e3123eb)
|
|
||||||
|
|
||||||
#### [v0.27.0](https://github.com/RhetTbull/osxphotos/compare/v0.26.1...v0.27.0)
|
#### [v0.27.0](https://github.com/RhetTbull/osxphotos/compare/v0.26.1...v0.27.0)
|
||||||
|
|
||||||
@@ -191,8 +262,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Update README.md [`#95`](https://github.com/RhetTbull/osxphotos/pull/95)
|
- Update README.md [`#95`](https://github.com/RhetTbull/osxphotos/pull/95)
|
||||||
- Added tests and README for AlbumInfo and FolderInfo [`d6a22b7`](https://github.com/RhetTbull/osxphotos/commit/d6a22b765ab17f6ef1ba8c50b77946f090979968)
|
- Added tests and README for AlbumInfo and FolderInfo [`d6a22b7`](https://github.com/RhetTbull/osxphotos/commit/d6a22b765ab17f6ef1ba8c50b77946f090979968)
|
||||||
- Added albuminfo.py for AlbumInfo and FolderInfo classes [`9636572`](https://github.com/RhetTbull/osxphotos/commit/96365728c2ff42abfb6828872ffac53b4c3c8024)
|
- Added albuminfo.py for AlbumInfo and FolderInfo classes [`9636572`](https://github.com/RhetTbull/osxphotos/commit/96365728c2ff42abfb6828872ffac53b4c3c8024)
|
||||||
- Updated CHANGELOG.md [`cde56e9`](https://github.com/RhetTbull/osxphotos/commit/cde56e9d13baf3098ec85839cf1aaa33b4915ac9)
|
|
||||||
- Update README.md TOC [`8544667`](https://github.com/RhetTbull/osxphotos/commit/8544667c729ea0d7fe39671d909e09cda519e250)
|
- Update README.md TOC [`8544667`](https://github.com/RhetTbull/osxphotos/commit/8544667c729ea0d7fe39671d909e09cda519e250)
|
||||||
|
- Update README.md [`1aa3838`](https://github.com/RhetTbull/osxphotos/commit/1aa3838c3866a18084ffe822de02df0eda464d71)
|
||||||
|
|
||||||
#### [v0.26.1](https://github.com/RhetTbull/osxphotos/compare/v0.26.0...v0.26.1)
|
#### [v0.26.1](https://github.com/RhetTbull/osxphotos/compare/v0.26.0...v0.26.1)
|
||||||
|
|
||||||
@@ -200,7 +271,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Bug fix for PhotosDB.photos() query [`1c9da5e`](https://github.com/RhetTbull/osxphotos/commit/1c9da5ed6ffa21f0577906b65b7da08951725d1f)
|
- Bug fix for PhotosDB.photos() query [`1c9da5e`](https://github.com/RhetTbull/osxphotos/commit/1c9da5ed6ffa21f0577906b65b7da08951725d1f)
|
||||||
- Updated test library [`d74f7f4`](https://github.com/RhetTbull/osxphotos/commit/d74f7f499bf59f37ec81cfa9d49cbbf3aafb5961)
|
- Updated test library [`d74f7f4`](https://github.com/RhetTbull/osxphotos/commit/d74f7f499bf59f37ec81cfa9d49cbbf3aafb5961)
|
||||||
- Updated CHANGELOG.md [`c85bb02`](https://github.com/RhetTbull/osxphotos/commit/c85bb023042e072d6688060eb259156c2fa579b9)
|
|
||||||
|
|
||||||
#### [v0.26.0](https://github.com/RhetTbull/osxphotos/compare/v0.25.1...v0.26.0)
|
#### [v0.26.0](https://github.com/RhetTbull/osxphotos/compare/v0.25.1...v0.26.0)
|
||||||
|
|
||||||
@@ -208,7 +278,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Added test for 10.15.4 [`1820715`](https://github.com/RhetTbull/osxphotos/commit/182071584904d001a9b199eef5febfb79e00696e)
|
- Added test for 10.15.4 [`1820715`](https://github.com/RhetTbull/osxphotos/commit/182071584904d001a9b199eef5febfb79e00696e)
|
||||||
- Changed PhotosDB albums interface as prep for adding folders [`3e50626`](https://github.com/RhetTbull/osxphotos/commit/3e5062684ab6d706d91d4abeb4e3b0ca47867b70)
|
- Changed PhotosDB albums interface as prep for adding folders [`3e50626`](https://github.com/RhetTbull/osxphotos/commit/3e5062684ab6d706d91d4abeb4e3b0ca47867b70)
|
||||||
- Updated CHANGELOG.md [`a6ca3f4`](https://github.com/RhetTbull/osxphotos/commit/a6ca3f453ce0fae4e8d13c7c256ed69a16d2e3f2)
|
- Update README.md [`626e460`](https://github.com/RhetTbull/osxphotos/commit/626e460aabb97b30af87cea2ec4f93e5fb925bec)
|
||||||
|
|
||||||
#### [v0.25.1](https://github.com/RhetTbull/osxphotos/compare/v0.25.0...v0.25.1)
|
#### [v0.25.1](https://github.com/RhetTbull/osxphotos/compare/v0.25.0...v0.25.1)
|
||||||
|
|
||||||
@@ -227,7 +297,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Added {album}, {keyword}, and {person} to template system [`507c4a3`](https://github.com/RhetTbull/osxphotos/commit/507c4a374014f999ca19789bce0df0c14332e021)
|
- Added {album}, {keyword}, and {person} to template system [`507c4a3`](https://github.com/RhetTbull/osxphotos/commit/507c4a374014f999ca19789bce0df0c14332e021)
|
||||||
- Added places command to CLI [`fd5e748`](https://github.com/RhetTbull/osxphotos/commit/fd5e748dca759ea1c3a7329d447f363afe8418b7)
|
- Added places command to CLI [`fd5e748`](https://github.com/RhetTbull/osxphotos/commit/fd5e748dca759ea1c3a7329d447f363afe8418b7)
|
||||||
- Updated export example [`01cd7fe`](https://github.com/RhetTbull/osxphotos/commit/01cd7fed6d7fc0c61c171a05319c211eb0a9f7c1)
|
- Updated export example [`01cd7fe`](https://github.com/RhetTbull/osxphotos/commit/01cd7fed6d7fc0c61c171a05319c211eb0a9f7c1)
|
||||||
- Updated CHANGELOG.md [`daea30f`](https://github.com/RhetTbull/osxphotos/commit/daea30f1626a208209ab6854cbd3b12f4b0a3405)
|
- Fixed typo in help text [`c02953e`](https://github.com/RhetTbull/osxphotos/commit/c02953ef5fe1aee219e0557bfd8c3322f1900a81)
|
||||||
|
|
||||||
#### [v0.24.2](https://github.com/RhetTbull/osxphotos/compare/v0.24.1...v0.24.2)
|
#### [v0.24.2](https://github.com/RhetTbull/osxphotos/compare/v0.24.1...v0.24.2)
|
||||||
|
|
||||||
@@ -248,8 +318,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 22 March 2020
|
> 22 March 2020
|
||||||
|
|
||||||
- Added export_by_album.py to examples [`908fead`](https://github.com/RhetTbull/osxphotos/commit/908fead8a2fbcef3b4a387f34d83d88c507c5939)
|
- Added export_by_album.py to examples [`908fead`](https://github.com/RhetTbull/osxphotos/commit/908fead8a2fbcef3b4a387f34d83d88c507c5939)
|
||||||
- Updated CHANGELOG.md [`072e894`](https://github.com/RhetTbull/osxphotos/commit/072e894e56c4dfe5522d073b202933fed0204ef5)
|
|
||||||
- Updated pathvalidate calls [`d066435`](https://github.com/RhetTbull/osxphotos/commit/d066435e3df4062be6a0a3d5fa7308f293e764d5)
|
- Updated pathvalidate calls [`d066435`](https://github.com/RhetTbull/osxphotos/commit/d066435e3df4062be6a0a3d5fa7308f293e764d5)
|
||||||
|
- Updated example [`8f0307f`](https://github.com/RhetTbull/osxphotos/commit/8f0307fc24345ca0e87017ac76791c9bbe8db25e)
|
||||||
|
|
||||||
#### [v0.23.3](https://github.com/RhetTbull/osxphotos/compare/v0.23.1...v0.23.3)
|
#### [v0.23.3](https://github.com/RhetTbull/osxphotos/compare/v0.23.1...v0.23.3)
|
||||||
|
|
||||||
@@ -264,15 +334,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 21 March 2020
|
> 21 March 2020
|
||||||
|
|
||||||
- Fixed requirements.txt for bplist2 [`cda5f44`](https://github.com/RhetTbull/osxphotos/commit/cda5f446933ea2272409d1f153e2a7811626ada6)
|
- Fixed requirements.txt for bplist2 [`cda5f44`](https://github.com/RhetTbull/osxphotos/commit/cda5f446933ea2272409d1f153e2a7811626ada6)
|
||||||
- Updated CHANGELOG.md [`b8da976`](https://github.com/RhetTbull/osxphotos/commit/b8da9765b8949eb90852d249c2877eeb1806d987)
|
|
||||||
- Updated requirements.txt [`9da7ad6`](https://github.com/RhetTbull/osxphotos/commit/9da7ad6dcc021fdafe358d74e1c52f69dc49ade8)
|
- Updated requirements.txt [`9da7ad6`](https://github.com/RhetTbull/osxphotos/commit/9da7ad6dcc021fdafe358d74e1c52f69dc49ade8)
|
||||||
|
- still trying to debug github actions fail [`960487f`](https://github.com/RhetTbull/osxphotos/commit/960487f2961f97f6b24d253472dcedf74dfc7797)
|
||||||
|
|
||||||
#### [v0.23.0](https://github.com/RhetTbull/osxphotos/compare/v0.22.23...v0.23.0)
|
#### [v0.23.0](https://github.com/RhetTbull/osxphotos/compare/v0.22.23...v0.23.0)
|
||||||
|
|
||||||
> 21 March 2020
|
> 21 March 2020
|
||||||
|
|
||||||
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`21547a8`](https://github.com/RhetTbull/osxphotos/commit/21547a8eaad117b11bc5e4dddf95436a8244e9ba)
|
||||||
- Added PhotoInfo.place for reverse geolocation data [`b338b34`](https://github.com/RhetTbull/osxphotos/commit/b338b34d5055a7621e4ebe4fbbae12227d77af6d)
|
- Added PhotoInfo.place for reverse geolocation data [`b338b34`](https://github.com/RhetTbull/osxphotos/commit/b338b34d5055a7621e4ebe4fbbae12227d77af6d)
|
||||||
- Updated CHANGELOG.md [`816b98e`](https://github.com/RhetTbull/osxphotos/commit/816b98e617c30d0bdb51bc2413f9915742c8592e)
|
|
||||||
- Update pythonpackage.yml [`92e5bdd`](https://github.com/RhetTbull/osxphotos/commit/92e5bdd2e986e5de2a710abf60ba0dc99c6a6730)
|
- Update pythonpackage.yml [`92e5bdd`](https://github.com/RhetTbull/osxphotos/commit/92e5bdd2e986e5de2a710abf60ba0dc99c6a6730)
|
||||||
|
|
||||||
#### [v0.22.23](https://github.com/RhetTbull/osxphotos/compare/v0.22.21...v0.22.23)
|
#### [v0.22.23](https://github.com/RhetTbull/osxphotos/compare/v0.22.21...v0.22.23)
|
||||||
@@ -288,7 +358,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Working on export edited bug for issue #78 [`8542e1a`](https://github.com/RhetTbull/osxphotos/commit/8542e1a97f6b640f287b37af9e50fd05f964ec4d)
|
- Working on export edited bug for issue #78 [`8542e1a`](https://github.com/RhetTbull/osxphotos/commit/8542e1a97f6b640f287b37af9e50fd05f964ec4d)
|
||||||
- Fixed download-missing to only download when actually missing [`dd20b8d`](https://github.com/RhetTbull/osxphotos/commit/dd20b8d8ac3b16d3b72a26b97dcc620b11e3a7c0)
|
- Fixed download-missing to only download when actually missing [`dd20b8d`](https://github.com/RhetTbull/osxphotos/commit/dd20b8d8ac3b16d3b72a26b97dcc620b11e3a7c0)
|
||||||
- Updated CHANGELOG.md [`cc9220e`](https://github.com/RhetTbull/osxphotos/commit/cc9220e0763816d784f2fd8377dfe14a99981622)
|
- test library updates [`e99391a`](https://github.com/RhetTbull/osxphotos/commit/e99391a68e844adb63edde3efb921cffa3928aeb)
|
||||||
|
|
||||||
#### [v0.22.17](https://github.com/RhetTbull/osxphotos/compare/v0.22.16...v0.22.17)
|
#### [v0.22.17](https://github.com/RhetTbull/osxphotos/compare/v0.22.16...v0.22.17)
|
||||||
|
|
||||||
@@ -303,18 +373,17 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- removed activate from --download-missing-photos Applescript, closes #69 [`#69`](https://github.com/RhetTbull/osxphotos/issues/69)
|
- removed activate from --download-missing-photos Applescript, closes #69 [`#69`](https://github.com/RhetTbull/osxphotos/issues/69)
|
||||||
- Added media type specials to json and string output, closes #68 [`#68`](https://github.com/RhetTbull/osxphotos/issues/68)
|
- Added media type specials to json and string output, closes #68 [`#68`](https://github.com/RhetTbull/osxphotos/issues/68)
|
||||||
|
- Fixed bug in --download-missing related to burst images [`1f13ba8`](https://github.com/RhetTbull/osxphotos/commit/1f13ba837fe36ff4eeb48cca02f5312a88a0a765)
|
||||||
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`dc87194`](https://github.com/RhetTbull/osxphotos/commit/dc87194eec252461d0cc0891b9ede4157125e828)
|
||||||
- Added query/export options for special media types [`2b7d84a`](https://github.com/RhetTbull/osxphotos/commit/2b7d84a4d103982ad874d875bafbc34d654d539a)
|
- Added query/export options for special media types [`2b7d84a`](https://github.com/RhetTbull/osxphotos/commit/2b7d84a4d103982ad874d875bafbc34d654d539a)
|
||||||
- README.md update [`a27ce33`](https://github.com/RhetTbull/osxphotos/commit/a27ce33473df3260dfb7ed26e28295cbf87d1e78)
|
- README.md update [`a27ce33`](https://github.com/RhetTbull/osxphotos/commit/a27ce33473df3260dfb7ed26e28295cbf87d1e78)
|
||||||
- Test library updates [`2d7d0b8`](https://github.com/RhetTbull/osxphotos/commit/2d7d0b86e0008cae043e314937504f36ad882990)
|
- Test library updates [`2d7d0b8`](https://github.com/RhetTbull/osxphotos/commit/2d7d0b86e0008cae043e314937504f36ad882990)
|
||||||
- Fixed bug in --download-missing related to burst images [`1f13ba8`](https://github.com/RhetTbull/osxphotos/commit/1f13ba837fe36ff4eeb48cca02f5312a88a0a765)
|
|
||||||
- test library update [`acb6b9e`](https://github.com/RhetTbull/osxphotos/commit/acb6b9e72f7f6b8f4f1d64b46f270a4d3e984fef)
|
|
||||||
|
|
||||||
#### [v0.22.13](https://github.com/RhetTbull/osxphotos/compare/v0.22.12...v0.22.13)
|
#### [v0.22.13](https://github.com/RhetTbull/osxphotos/compare/v0.22.12...v0.22.13)
|
||||||
|
|
||||||
> 8 March 2020
|
> 8 March 2020
|
||||||
|
|
||||||
- Added media type specials, closes #60 [`#60`](https://github.com/RhetTbull/osxphotos/issues/60)
|
- Added media type specials, closes #60 [`#60`](https://github.com/RhetTbull/osxphotos/issues/60)
|
||||||
- Updated CHANGELOG.md [`08a9793`](https://github.com/RhetTbull/osxphotos/commit/08a9793651481e1984a4482794ffedd48e4367a2)
|
|
||||||
- Updated README.md [`1f8fd6e`](https://github.com/RhetTbull/osxphotos/commit/1f8fd6e929cc0edd3dd2f222416454d26955bf2a)
|
- Updated README.md [`1f8fd6e`](https://github.com/RhetTbull/osxphotos/commit/1f8fd6e929cc0edd3dd2f222416454d26955bf2a)
|
||||||
|
|
||||||
#### [v0.22.12](https://github.com/RhetTbull/osxphotos/compare/0.22.10...v0.22.12)
|
#### [v0.22.12](https://github.com/RhetTbull/osxphotos/compare/0.22.10...v0.22.12)
|
||||||
@@ -331,7 +400,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Fixed bug in --download-missing to fix issue #64 [`c654e3d`](https://github.com/RhetTbull/osxphotos/commit/c654e3dc61283382b37b6892dab1516ec517143a)
|
- Fixed bug in --download-missing to fix issue #64 [`c654e3d`](https://github.com/RhetTbull/osxphotos/commit/c654e3dc61283382b37b6892dab1516ec517143a)
|
||||||
- removed commented out code [`69addc3`](https://github.com/RhetTbull/osxphotos/commit/69addc34649f992c6a4a0e0e334754a72530f0ba)
|
- removed commented out code [`69addc3`](https://github.com/RhetTbull/osxphotos/commit/69addc34649f992c6a4a0e0e334754a72530f0ba)
|
||||||
- Updated CHANGELOG.md [`1e013b6`](https://github.com/RhetTbull/osxphotos/commit/1e013b6802e49e26ec5a94eb702e841b2eb68395)
|
- Cleaned up comments and unneeded test code [`e3c40bc`](https://github.com/RhetTbull/osxphotos/commit/e3c40bcbaaf3560d53091cf46ed851d90ff82cfa)
|
||||||
|
|
||||||
#### [v0.22.9](https://github.com/RhetTbull/osxphotos/compare/v0.22.7...v0.22.9)
|
#### [v0.22.9](https://github.com/RhetTbull/osxphotos/compare/v0.22.7...v0.22.9)
|
||||||
|
|
||||||
@@ -343,7 +412,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Test library updates [`6e563e2`](https://github.com/RhetTbull/osxphotos/commit/6e563e214c569ba7838f7464de9258c3bba5db23)
|
- Test library updates [`6e563e2`](https://github.com/RhetTbull/osxphotos/commit/6e563e214c569ba7838f7464de9258c3bba5db23)
|
||||||
- Removed _tmp_file code that's no longer needed [`27994c9`](https://github.com/RhetTbull/osxphotos/commit/27994c9fd372303833a5794f1de9815f425c762e)
|
- Removed _tmp_file code that's no longer needed [`27994c9`](https://github.com/RhetTbull/osxphotos/commit/27994c9fd372303833a5794f1de9815f425c762e)
|
||||||
- Updated photos_repl.py [`fdf636a`](https://github.com/RhetTbull/osxphotos/commit/fdf636ac8864ebb2cc324b1f9d3c6c82ee3910f9)
|
- Updated photos_repl.py [`fdf636a`](https://github.com/RhetTbull/osxphotos/commit/fdf636ac8864ebb2cc324b1f9d3c6c82ee3910f9)
|
||||||
- Updated CHANGELOG.md [`f910124`](https://github.com/RhetTbull/osxphotos/commit/f910124fe1fbf75d44c09c79607374bf000733a1)
|
- Added PhotosDB() behavior to open last library if no args passed but also added cautionary note to README [`46d3c7d`](https://github.com/RhetTbull/osxphotos/commit/46d3c7dbdaf848d5c340ce8a362ff296a36c552d)
|
||||||
|
|
||||||
#### [v0.22.7](https://github.com/RhetTbull/osxphotos/compare/v0.22.4...v0.22.7)
|
#### [v0.22.7](https://github.com/RhetTbull/osxphotos/compare/v0.22.4...v0.22.7)
|
||||||
|
|
||||||
@@ -357,7 +426,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Added date_modified to PhotoInfo [`67b0ae0`](https://github.com/RhetTbull/osxphotos/commit/67b0ae0bf679815372d415c3064e21d46a5b8718)
|
- Added date_modified to PhotoInfo [`67b0ae0`](https://github.com/RhetTbull/osxphotos/commit/67b0ae0bf679815372d415c3064e21d46a5b8718)
|
||||||
- Added date_modified to PhotoInfo [`4d36b3b`](https://github.com/RhetTbull/osxphotos/commit/4d36b3b31f3e0e74d9d111b6b691771e19f94086)
|
- Added date_modified to PhotoInfo [`4d36b3b`](https://github.com/RhetTbull/osxphotos/commit/4d36b3b31f3e0e74d9d111b6b691771e19f94086)
|
||||||
- Updated CLI options with more descriptive metavar names [`e79cb92`](https://github.com/RhetTbull/osxphotos/commit/e79cb92693758c984dc789d5fa5d2e87e381e921)
|
- Updated CLI options with more descriptive metavar names [`e79cb92`](https://github.com/RhetTbull/osxphotos/commit/e79cb92693758c984dc789d5fa5d2e87e381e921)
|
||||||
- CLI now looks for photos library to use if non specified by user [`50b7e69`](https://github.com/RhetTbull/osxphotos/commit/50b7e6920a694aa45f478d1131868525c9147919)
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`898d3af`](https://github.com/RhetTbull/osxphotos/commit/898d3afc0892546ece6c3d675208dea216e20633)
|
||||||
|
|
||||||
#### [v0.22.4](https://github.com/RhetTbull/osxphotos/compare/v0.22.0...v0.22.4)
|
#### [v0.22.4](https://github.com/RhetTbull/osxphotos/compare/v0.22.0...v0.22.4)
|
||||||
|
|
||||||
@@ -369,7 +438,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. [`cfa2b4a`](https://github.com/RhetTbull/osxphotos/commit/cfa2b4a828facf0aff5bc19f777457ad776c4a05)
|
- Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. [`cfa2b4a`](https://github.com/RhetTbull/osxphotos/commit/cfa2b4a828facf0aff5bc19f777457ad776c4a05)
|
||||||
- Refactored _query. Still hairy, but less so. [`b9dee49`](https://github.com/RhetTbull/osxphotos/commit/b9dee4995c6d89fadb3d2482374b7098f2ab5ed9)
|
- Refactored _query. Still hairy, but less so. [`b9dee49`](https://github.com/RhetTbull/osxphotos/commit/b9dee4995c6d89fadb3d2482374b7098f2ab5ed9)
|
||||||
- Updated README.md [`0aff83f`](https://github.com/RhetTbull/osxphotos/commit/0aff83ff21c20e293c0b75bacf2863090a0fb725)
|
- Updated README.md [`0aff83f`](https://github.com/RhetTbull/osxphotos/commit/0aff83ff21c20e293c0b75bacf2863090a0fb725)
|
||||||
- Started adding tests for CLI [`f0b18c3`](https://github.com/RhetTbull/osxphotos/commit/f0b18c3d29b2141d348be0495013c51c072c6251)
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`7150956`](https://github.com/RhetTbull/osxphotos/commit/7150956a488677d402a6d43443d04c4b11dc7be0)
|
||||||
|
|
||||||
#### [v0.22.0](https://github.com/RhetTbull/osxphotos/compare/v0.21.5...v0.22.0)
|
#### [v0.22.0](https://github.com/RhetTbull/osxphotos/compare/v0.21.5...v0.22.0)
|
||||||
|
|
||||||
@@ -377,7 +446,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
- Refactored PhotosDB and CLI to require explicity passing the database to avoid non-deterministic behavior when last database can't be found. This may break existing code. [`ede56ff`](https://github.com/RhetTbull/osxphotos/commit/ede56ffc31cf98811b3d4d16e22406ac0eae0315)
|
- Refactored PhotosDB and CLI to require explicity passing the database to avoid non-deterministic behavior when last database can't be found. This may break existing code. [`ede56ff`](https://github.com/RhetTbull/osxphotos/commit/ede56ffc31cf98811b3d4d16e22406ac0eae0315)
|
||||||
- Changed get_system_library_path to return None if could not get system library [`646ea4f`](https://github.com/RhetTbull/osxphotos/commit/646ea4f24ca1119b27280af1445e31adcd0690f0)
|
- Changed get_system_library_path to return None if could not get system library [`646ea4f`](https://github.com/RhetTbull/osxphotos/commit/646ea4f24ca1119b27280af1445e31adcd0690f0)
|
||||||
- Updated CHANGELOG.md [`bd20388`](https://github.com/RhetTbull/osxphotos/commit/bd20388778dfa645277029601c63fc9835b7a406)
|
- Fix to setup to specify versions of required packages [`de05323`](https://github.com/RhetTbull/osxphotos/commit/de05323a153fe49723b39e48b9038c1fb9535a72)
|
||||||
|
|
||||||
#### [v0.21.5](https://github.com/RhetTbull/osxphotos/compare/v0.21.0...v0.21.5)
|
#### [v0.21.5](https://github.com/RhetTbull/osxphotos/compare/v0.21.0...v0.21.5)
|
||||||
|
|
||||||
@@ -392,8 +461,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
> 4 January 2020
|
> 4 January 2020
|
||||||
|
|
||||||
- Added live photo support for both Photos 4 & 5 [`d5eaff0`](https://github.com/RhetTbull/osxphotos/commit/d5eaff02f2a29a9d105ab72e9a9aeffbc9a3425b)
|
- Added live photo support for both Photos 4 & 5 [`d5eaff0`](https://github.com/RhetTbull/osxphotos/commit/d5eaff02f2a29a9d105ab72e9a9aeffbc9a3425b)
|
||||||
- Added support for burst photos; added export-bursts to CLI [`593983a`](https://github.com/RhetTbull/osxphotos/commit/593983a09940e67fb9347bf345cfd7289465fa0a)
|
|
||||||
- Added live-photo option to CLI query and export [`6f6d37c`](https://github.com/RhetTbull/osxphotos/commit/6f6d37ceacf71a52a2c0216f0ad75afee244946a)
|
- Added live-photo option to CLI query and export [`6f6d37c`](https://github.com/RhetTbull/osxphotos/commit/6f6d37ceacf71a52a2c0216f0ad75afee244946a)
|
||||||
|
- Initial support for live photos (Photos 5 only) [`1a89a18`](https://github.com/RhetTbull/osxphotos/commit/1a89a18a011a25616d7a18fb9bf1270b0b206fb4)
|
||||||
|
|
||||||
#### [v0.20.0](https://github.com/RhetTbull/osxphotos/compare/v0.19.0...v0.20.0)
|
#### [v0.20.0](https://github.com/RhetTbull/osxphotos/compare/v0.19.0...v0.20.0)
|
||||||
|
|
||||||
@@ -407,9 +476,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
|
|
||||||
> 29 December 2019
|
> 29 December 2019
|
||||||
|
|
||||||
|
- Merge branch 'master' of https://github.com/RhetTbull/osxphotos [`51843fb`](https://github.com/RhetTbull/osxphotos/commit/51843fb46d6ce69456400271c97aa642466d5719)
|
||||||
- Added support for movies for Photos 5; fixed bugs in ismissing and path [`6f4d129`](https://github.com/RhetTbull/osxphotos/commit/6f4d129f07046c4a34d3d6cf6854c8514a594781)
|
- Added support for movies for Photos 5; fixed bugs in ismissing and path [`6f4d129`](https://github.com/RhetTbull/osxphotos/commit/6f4d129f07046c4a34d3d6cf6854c8514a594781)
|
||||||
- Added support for movies for Photos 5; fixed bugs in ismissing and path [`b030966`](https://github.com/RhetTbull/osxphotos/commit/b030966051af93be380ff967ac047bf566e5d817)
|
- Added support for movies for Photos 5; fixed bugs in ismissing and path [`b030966`](https://github.com/RhetTbull/osxphotos/commit/b030966051af93be380ff967ac047bf566e5d817)
|
||||||
- Initial support for movies [`dbe363e`](https://github.com/RhetTbull/osxphotos/commit/dbe363e4d754253a0405fb1df045677e8780d630)
|
|
||||||
|
|
||||||
#### [v0.18.0](https://github.com/RhetTbull/osxphotos/compare/v0.15.1...v0.18.0)
|
#### [v0.18.0](https://github.com/RhetTbull/osxphotos/compare/v0.15.1...v0.18.0)
|
||||||
|
|
||||||
@@ -423,11 +492,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|||||||
- Moved PhotosDB attributes to properties instead of methods [`d95acdf`](https://github.com/RhetTbull/osxphotos/commit/d95acdf9f8764a1720bcba71a6dad29bf668eaf9)
|
- Moved PhotosDB attributes to properties instead of methods [`d95acdf`](https://github.com/RhetTbull/osxphotos/commit/d95acdf9f8764a1720bcba71a6dad29bf668eaf9)
|
||||||
- changed interface for export, prepped for exiftool_json_sidecar [`1fe8859`](https://github.com/RhetTbull/osxphotos/commit/1fe885962e8a9a420e776bdd3dc640ca143224b2)
|
- changed interface for export, prepped for exiftool_json_sidecar [`1fe8859`](https://github.com/RhetTbull/osxphotos/commit/1fe885962e8a9a420e776bdd3dc640ca143224b2)
|
||||||
|
|
||||||
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.15.0...v0.15.1)
|
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.1)
|
||||||
|
|
||||||
> 31 May 2020
|
|
||||||
|
|
||||||
#### [v0.15.0](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.0)
|
|
||||||
|
|
||||||
> 14 December 2019
|
> 14 December 2019
|
||||||
|
|
||||||
|
|||||||
110
README.md
110
README.md
@@ -17,6 +17,7 @@
|
|||||||
+ [AlbumInfo](#albuminfo)
|
+ [AlbumInfo](#albuminfo)
|
||||||
+ [FolderInfo](#folderinfo)
|
+ [FolderInfo](#folderinfo)
|
||||||
+ [PlaceInfo](#placeinfo)
|
+ [PlaceInfo](#placeinfo)
|
||||||
|
+ [ScoreInfo](#scoreinfo)
|
||||||
+ [Template Substitutions](#template-substitutions)
|
+ [Template Substitutions](#template-substitutions)
|
||||||
+ [Utility Functions](#utility-functions)
|
+ [Utility Functions](#utility-functions)
|
||||||
* [Examples](#examples)
|
* [Examples](#examples)
|
||||||
@@ -34,7 +35,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
|
|
||||||
## Supported operating systems
|
## Supported operating systems
|
||||||
|
|
||||||
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 & 10.15.4 / Photos 5.0.
|
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 - 10.15.5 / Photos 5.0.
|
||||||
|
|
||||||
Requires python >= 3.8. You can probably get this to run with Python 3.6 or 3.7 (see notes [below](#Installation-instructions)) but only 3.8+ is officially supported.
|
Requires python >= 3.8. You can probably get this to run with Python 3.6 or 3.7 (see notes [below](#Installation-instructions)) but only 3.8+ is officially supported.
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ You can also install directly from [pypi](https://pypi.org/) but you must use py
|
|||||||
|
|
||||||
This package will install a command line utility called `osxphotos` that allows you to query the Photos database. Alternatively, you can also run the command line utility like this: `python3 -m osxphotos`
|
This package will install a command line utility called `osxphotos` that allows you to query the Photos database. Alternatively, you can also run the command line utility like this: `python3 -m osxphotos`
|
||||||
|
|
||||||
If you only care about the command line tool, I recommend installing with [pipx](https://github.com/pipxproject/pipx)
|
If you only care about the command line tool, you can download an executable of the latest [release](https://github.com/RhetTbull/osxphotos/releases). Alternatively, I recommend installing with [pipx](https://github.com/pipxproject/pipx)
|
||||||
|
|
||||||
After installing pipx:
|
After installing pipx:
|
||||||
`pipx install osxphotos`
|
`pipx install osxphotos`
|
||||||
@@ -90,6 +91,7 @@ Commands:
|
|||||||
help Print help; for help on commands: help <command>.
|
help Print help; for help on commands: help <command>.
|
||||||
info Print out descriptive info of the Photos library database.
|
info Print out descriptive info of the Photos library database.
|
||||||
keywords Print out keywords found in the Photos library.
|
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.
|
list Print list of Photos libraries found on the system.
|
||||||
persons Print out persons (faces) found in the Photos library.
|
persons Print out persons (faces) found in the Photos library.
|
||||||
places Print out places found in the Photos library.
|
places Print out places found in the Photos library.
|
||||||
@@ -125,13 +127,13 @@ Options:
|
|||||||
-V, --verbose Print verbose output.
|
-V, --verbose Print verbose output.
|
||||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||||
more than one keyword, treated as "OR", e.g.
|
more than one keyword, treated as "OR", e.g.
|
||||||
find photos match any keyword
|
find photos matching any keyword
|
||||||
--person PERSON Search for photos with person PERSON. If
|
--person PERSON Search for photos with person PERSON. If
|
||||||
more than one person, treated as "OR", e.g.
|
more than one person, treated as "OR", e.g.
|
||||||
find photos match any person
|
find photos matching any person
|
||||||
--album ALBUM Search for photos in album ALBUM. If more
|
--album ALBUM Search for photos in album ALBUM. If more
|
||||||
than one album, treated as "OR", e.g. find
|
than one album, treated as "OR", e.g. find
|
||||||
photos match any album
|
photos matching any album
|
||||||
--folder FOLDER Search for photos in an album in folder
|
--folder FOLDER Search for photos in an album in folder
|
||||||
FOLDER. If more than one folder, treated as
|
FOLDER. If more than one folder, treated as
|
||||||
"OR", e.g. find photos in any FOLDER. Only
|
"OR", e.g. find photos in any FOLDER. Only
|
||||||
@@ -146,11 +148,15 @@ Options:
|
|||||||
geolocation info
|
geolocation info
|
||||||
--no-place Search for photos with no associated place
|
--no-place Search for photos with no associated place
|
||||||
name info (no reverse geolocation info)
|
name info (no reverse geolocation info)
|
||||||
|
--label LABEL Search for photos with image classification
|
||||||
|
label LABEL (Photos 5 only). If more than
|
||||||
|
one label, treated as "OR", e.g. find photos
|
||||||
|
matching any label
|
||||||
--uti UTI Search for photos whose uniform type
|
--uti UTI Search for photos whose uniform type
|
||||||
identifier (UTI) matches UTI
|
identifier (UTI) matches UTI
|
||||||
-i, --ignore-case Case insensitive search for title,
|
-i, --ignore-case Case insensitive search for title,
|
||||||
description, or place. Does not apply to
|
description, place, keyword, person, or
|
||||||
keyword, person, or album.
|
album.
|
||||||
--edited Search for photos that have been edited.
|
--edited Search for photos that have been edited.
|
||||||
--external-edit Search for photos edited in external editor.
|
--external-edit Search for photos edited in external editor.
|
||||||
--favorite Search for photos marked favorite.
|
--favorite Search for photos marked favorite.
|
||||||
@@ -367,7 +373,8 @@ contain a brace symbol ('{' or '}').
|
|||||||
|
|
||||||
If you do not specify a default value and the template substitution has no
|
If you do not specify a default value and the template substitution has no
|
||||||
value, '_' (underscore) will be used as the default value. For example, in the
|
value, '_' (underscore) will be used as the default value. For example, in the
|
||||||
above example, this would result in '2020/_/photoname.jpg' if address was null.
|
above example, this would result in '2020/_/photoname.jpg' if address was
|
||||||
|
null.
|
||||||
|
|
||||||
Substitution Description
|
Substitution Description
|
||||||
{name} Current filename of the photo
|
{name} Current filename of the photo
|
||||||
@@ -391,6 +398,18 @@ Substitution Description
|
|||||||
creation time
|
creation time
|
||||||
{created.doy} 3-digit day of year (e.g Julian day) of file
|
{created.doy} 3-digit day of year (e.g Julian day) of file
|
||||||
creation time, starting from 1 (zero padded)
|
creation time, starting from 1 (zero padded)
|
||||||
|
{created.hour} 2-digit hour of the file creation time
|
||||||
|
{created.min} 2-digit minute of the file creation time
|
||||||
|
{created.sec} 2-digit second of the file creation time
|
||||||
|
{created.strftime} Apply strftime template to file creation
|
||||||
|
date/time. Should be used in form
|
||||||
|
{created.strftime,TEMPLATE} where TEMPLATE
|
||||||
|
is a valid strftime template, e.g.
|
||||||
|
{created.strftime,%Y-%U} would result in
|
||||||
|
year-week number of year: '2020-23'. If used
|
||||||
|
with no template will return null value. See
|
||||||
|
https://strftime.org/ for help on strftime
|
||||||
|
templates.
|
||||||
{modified.date} Photo's modification date in ISO format,
|
{modified.date} Photo's modification date in ISO format,
|
||||||
e.g. '2020-03-22'
|
e.g. '2020-03-22'
|
||||||
{modified.year} 4-digit year of file modification time
|
{modified.year} 4-digit year of file modification time
|
||||||
@@ -406,6 +425,9 @@ Substitution Description
|
|||||||
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
||||||
modification time, starting from 1 (zero
|
modification time, starting from 1 (zero
|
||||||
padded)
|
padded)
|
||||||
|
{modified.hour} 2-digit hour of the file modification time
|
||||||
|
{modified.min} 2-digit minute of the file modification time
|
||||||
|
{modified.sec} 2-digit second of the file modification time
|
||||||
{place.name} Place name from the photo's reverse
|
{place.name} Place name from the photo's reverse
|
||||||
geolocation data, as displayed in Photos
|
geolocation data, as displayed in Photos
|
||||||
{place.country_code} The ISO country code from the photo's
|
{place.country_code} The ISO country code from the photo's
|
||||||
@@ -807,6 +829,23 @@ photosdb.db_version
|
|||||||
|
|
||||||
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
|
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
|
||||||
|
|
||||||
|
#### `get_db_connection()`
|
||||||
|
Returns tuple of (connection, cursor) for the working copy of the Photos database. This is useful for debugging or prototyping new features.
|
||||||
|
|
||||||
|
```python
|
||||||
|
photosdb = osxphotos.PhotosDB()
|
||||||
|
conn, cursor = photosdb.get_db_connection()
|
||||||
|
|
||||||
|
results = conn.execute(
|
||||||
|
"SELECT ZUUID FROM ZGENERICASSET WHERE ZFAVORITE = 1;"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for row in results:
|
||||||
|
# do something
|
||||||
|
pass
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
|
||||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
|
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
|
||||||
|
|
||||||
@@ -1128,7 +1167,12 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo")
|
|||||||
photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach")
|
photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.as_dict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`.
|
**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.as_dict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`.
|
||||||
|
|
||||||
|
#### `score`
|
||||||
|
Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the computed aesthetic scores for each photo.
|
||||||
|
|
||||||
|
**Note**: Valid only for Photos 5; returns None for earlier Photos versions.
|
||||||
|
|
||||||
#### `json()`
|
#### `json()`
|
||||||
Returns a JSON representation of all photo info
|
Returns a JSON representation of all photo info
|
||||||
@@ -1374,6 +1418,45 @@ PostalAddress(street='3700 Wailea Alanui Dr', sub_locality=None, city='Kihei', s
|
|||||||
>>> photo.place.address.postal_code
|
>>> photo.place.address.postal_code
|
||||||
'96753'
|
'96753'
|
||||||
```
|
```
|
||||||
|
### ScoreInfo
|
||||||
|
[PhotoInfo.score](#score) returns a ScoreInfo object that exposes the computed aesthetic scores for each photo (**Photos 5 only**). I have not yet reverse engineered the meaning of each score. The `overall` score seems to the most useful and appears to be a composite of the other scores. The following score properties are currently available:
|
||||||
|
|
||||||
|
```python
|
||||||
|
overall: float
|
||||||
|
curation: float
|
||||||
|
promotion: float
|
||||||
|
highlight_visibility: float
|
||||||
|
behavioral: float
|
||||||
|
failure: float
|
||||||
|
harmonious_color: float
|
||||||
|
immersiveness: float
|
||||||
|
interaction: float
|
||||||
|
interesting_subject: float
|
||||||
|
intrusive_object_presence: float
|
||||||
|
lively_color: float
|
||||||
|
low_light: float
|
||||||
|
noise: float
|
||||||
|
pleasant_camera_tilt: float
|
||||||
|
pleasant_composition: float
|
||||||
|
pleasant_lighting: float
|
||||||
|
pleasant_pattern: float
|
||||||
|
pleasant_perspective: float
|
||||||
|
pleasant_post_processing: float
|
||||||
|
pleasant_reflection: float
|
||||||
|
pleasant_symmetry: float
|
||||||
|
sharply_focused_subject: float
|
||||||
|
tastefully_blurred: float
|
||||||
|
well_chosen_subject: float
|
||||||
|
well_framed_subject: float
|
||||||
|
well_timed_shot: float
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: find your "best" photo of food
|
||||||
|
```python
|
||||||
|
>>> import osxphotos
|
||||||
|
>>> photos = osxphotos.PhotosDB().photos()
|
||||||
|
>>> best_food_photo = sorted([p for p in photos if "food" in p.labels_normalized], key=lambda p: p.score.overall, reverse=True)[0]
|
||||||
|
```
|
||||||
|
|
||||||
### Template Substitutions
|
### Template Substitutions
|
||||||
|
|
||||||
@@ -1392,7 +1475,12 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
|||||||
|{created.month}|Month name in user's locale of the file creation time|
|
|{created.month}|Month name in user's locale of the file creation time|
|
||||||
|{created.mon}|Month abbreviation in the user's locale of the file creation time|
|
|{created.mon}|Month abbreviation in the user's locale of the file creation time|
|
||||||
|{created.dd}|2-digit day of the month (zero padded) of file creation time|
|
|{created.dd}|2-digit day of the month (zero padded) of file creation time|
|
||||||
|
|{created.dow}|Day of week in user's locale of the file creation time|
|
||||||
|{created.doy}|3-digit day of year (e.g Julian day) of file creation time, starting from 1 (zero padded)|
|
|{created.doy}|3-digit day of year (e.g Julian day) of file creation time, starting from 1 (zero padded)|
|
||||||
|
|{created.hour}|2-digit hour of the file creation time|
|
||||||
|
|{created.min}|2-digit minute of the file creation time|
|
||||||
|
|{created.sec}|2-digit second of the file creation time|
|
||||||
|
|{created.strftime}|Apply strftime template to file creation date/time. Should be used in form {created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See https://strftime.org/ for help on strftime templates.|
|
||||||
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'|
|
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'|
|
||||||
|{modified.year}|4-digit year of file modification time|
|
|{modified.year}|4-digit year of file modification time|
|
||||||
|{modified.yy}|2-digit year of file modification time|
|
|{modified.yy}|2-digit year of file modification time|
|
||||||
@@ -1401,6 +1489,9 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
|||||||
|{modified.mon}|Month abbreviation in the user's locale of the file modification time|
|
|{modified.mon}|Month abbreviation in the user's locale of the file modification time|
|
||||||
|{modified.dd}|2-digit day of the month (zero padded) of the file modification time|
|
|{modified.dd}|2-digit day of the month (zero padded) of the file modification time|
|
||||||
|{modified.doy}|3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)|
|
|{modified.doy}|3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)|
|
||||||
|
|{modified.hour}|2-digit hour of the file modification time|
|
||||||
|
|{modified.min}|2-digit minute of the file modification time|
|
||||||
|
|{modified.sec}|2-digit second of the file modification time|
|
||||||
|{place.name}|Place name from the photo's reverse geolocation data, as displayed in Photos|
|
|{place.name}|Place name from the photo's reverse geolocation data, as displayed in Photos|
|
||||||
|{place.country_code}|The ISO country code from the photo's reverse geolocation data|
|
|{place.country_code}|The ISO country code from the photo's reverse geolocation data|
|
||||||
|{place.name.country}|Country name from the photo's reverse geolocation data|
|
|{place.name.country}|Country name from the photo's reverse geolocation data|
|
||||||
@@ -1421,7 +1512,6 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
|||||||
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
||||||
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|
||||||
|
|
||||||
|
|
||||||
### Utility Functions
|
### Utility Functions
|
||||||
|
|
||||||
The following functions are located in osxphotos.utils
|
The following functions are located in osxphotos.utils
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# If you run this using python from command line, do so with -i flag:
|
# If you run this using python from command line, do so with -i flag:
|
||||||
# python3 -i examples/photos_repl.py
|
# python3 -i examples/photos_repl.py
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -17,14 +18,27 @@ import osxphotos
|
|||||||
from osxphotos.__main__ import get_photos_db, _list_libraries
|
from osxphotos.__main__ import get_photos_db, _list_libraries
|
||||||
|
|
||||||
|
|
||||||
|
def show(photo):
|
||||||
|
""" open image with default image viewer
|
||||||
|
|
||||||
|
Note: This is for debugging only -- it will actually open any filetype which could
|
||||||
|
be very, very bad.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: PhotoInfo object or a path to a photo on disk
|
||||||
|
"""
|
||||||
|
photopath = photo.path if isinstance(photo, osxphotos.PhotoInfo) else photo
|
||||||
|
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
return f"'{photopath}' does not appear to be a valid photo path"
|
||||||
|
|
||||||
|
os.system(f"open '{photopath}'")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
db = None
|
db = None
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
db = sys.argv[1] if len(sys.argv) > 1 else get_photos_db()
|
||||||
db = sys.argv[1]
|
|
||||||
else:
|
|
||||||
db = get_photos_db()
|
|
||||||
|
|
||||||
if db:
|
if db:
|
||||||
print("loading database")
|
print("loading database")
|
||||||
tic = time.perf_counter()
|
tic = time.perf_counter()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import pprint
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@ def query_options(f):
|
|||||||
default=None,
|
default=None,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Search for photos with keyword KEYWORD. "
|
help="Search for photos with keyword KEYWORD. "
|
||||||
'If more than one keyword, treated as "OR", e.g. find photos match any keyword',
|
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
|
||||||
),
|
),
|
||||||
o(
|
o(
|
||||||
"--person",
|
"--person",
|
||||||
@@ -252,7 +253,7 @@ def query_options(f):
|
|||||||
default=None,
|
default=None,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Search for photos with person PERSON. "
|
help="Search for photos with person PERSON. "
|
||||||
'If more than one person, treated as "OR", e.g. find photos match any person',
|
'If more than one person, treated as "OR", e.g. find photos matching any person',
|
||||||
),
|
),
|
||||||
o(
|
o(
|
||||||
"--album",
|
"--album",
|
||||||
@@ -260,7 +261,7 @@ def query_options(f):
|
|||||||
default=None,
|
default=None,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Search for photos in album ALBUM. "
|
help="Search for photos in album ALBUM. "
|
||||||
'If more than one album, treated as "OR", e.g. find photos match any album',
|
'If more than one album, treated as "OR", e.g. find photos matching any album',
|
||||||
),
|
),
|
||||||
o(
|
o(
|
||||||
"--folder",
|
"--folder",
|
||||||
@@ -310,6 +311,13 @@ def query_options(f):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
||||||
),
|
),
|
||||||
|
o(
|
||||||
|
"--label",
|
||||||
|
metavar="LABEL",
|
||||||
|
multiple=True,
|
||||||
|
help="Search for photos with image classification label LABEL (Photos 5 only). "
|
||||||
|
'If more than one label, treated as "OR", e.g. find photos matching any label',
|
||||||
|
),
|
||||||
o(
|
o(
|
||||||
"--uti",
|
"--uti",
|
||||||
metavar="UTI",
|
metavar="UTI",
|
||||||
@@ -321,7 +329,7 @@ def query_options(f):
|
|||||||
"-i",
|
"-i",
|
||||||
"--ignore-case",
|
"--ignore-case",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Case insensitive search for title, description, or place. Does not apply to keyword, person, or album.",
|
help="Case insensitive search for title, description, place, keyword, person, or album.",
|
||||||
),
|
),
|
||||||
o("--edited", is_flag=True, help="Search for photos that have been edited."),
|
o("--edited", is_flag=True, help="Search for photos that have been edited."),
|
||||||
o(
|
o(
|
||||||
@@ -442,6 +450,81 @@ def cli(ctx, db, json_, debug):
|
|||||||
ctx.obj = CLI_Obj(db=db, json=json_, debug=debug)
|
ctx.obj = CLI_Obj(db=db, json=json_, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(hidden=True)
|
||||||
|
@DB_OPTION
|
||||||
|
@DB_ARGUMENT
|
||||||
|
@click.option(
|
||||||
|
"--dump",
|
||||||
|
metavar="ATTR",
|
||||||
|
help="Name of PhotosDB attribute to print; "
|
||||||
|
+ "can also use albums, persons, keywords, photos to dump related attributes.",
|
||||||
|
multiple=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--uuid",
|
||||||
|
metavar="UUID",
|
||||||
|
help="Use with '--dump photos' to dump only certain UUIDs",
|
||||||
|
multiple=True,
|
||||||
|
)
|
||||||
|
@click.pass_obj
|
||||||
|
@click.pass_context
|
||||||
|
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
|
||||||
|
""" Print out debug info """
|
||||||
|
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||||
|
if db is None:
|
||||||
|
click.echo(cli.commands["debug-dump"].get_help(ctx), err=True)
|
||||||
|
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||||
|
_list_libraries()
|
||||||
|
return
|
||||||
|
|
||||||
|
start_t = time.perf_counter()
|
||||||
|
print(f"Opening database: {db}")
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||||
|
stop_t = time.perf_counter()
|
||||||
|
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
||||||
|
|
||||||
|
for attr in dump:
|
||||||
|
if attr == "albums":
|
||||||
|
print("_dbalbums_album:")
|
||||||
|
pprint.pprint(photosdb._dbalbums_album)
|
||||||
|
print("_dbalbums_uuid:")
|
||||||
|
pprint.pprint(photosdb._dbalbums_uuid)
|
||||||
|
print("_dbalbum_details:")
|
||||||
|
pprint.pprint(photosdb._dbalbum_details)
|
||||||
|
print("_dbalbum_folders:")
|
||||||
|
pprint.pprint(photosdb._dbalbum_folders)
|
||||||
|
print("_dbfolder_details:")
|
||||||
|
pprint.pprint(photosdb._dbfolder_details)
|
||||||
|
elif attr == "keywords":
|
||||||
|
print("_dbkeywords_keyword:")
|
||||||
|
pprint.pprint(photosdb._dbkeywords_keyword)
|
||||||
|
print("_dbkeywords_uuid:")
|
||||||
|
pprint.pprint(photosdb._dbkeywords_uuid)
|
||||||
|
elif attr == "persons":
|
||||||
|
print("_dbfaces_person:")
|
||||||
|
pprint.pprint(photosdb._dbfaces_person)
|
||||||
|
print("_dbfaces_uuid:")
|
||||||
|
pprint.pprint(photosdb._dbfaces_uuid)
|
||||||
|
elif attr == "photos":
|
||||||
|
if uuid:
|
||||||
|
for uuid_ in uuid:
|
||||||
|
print(f"_dbphotos['{uuid_}']:")
|
||||||
|
try:
|
||||||
|
pprint.pprint(photosdb._dbphotos[uuid_])
|
||||||
|
except KeyError:
|
||||||
|
print(f"Did not find uuid {uuid_} in _dbphotos")
|
||||||
|
else:
|
||||||
|
print("_dbphotos:")
|
||||||
|
pprint.pprint(photosdb._dbphotos)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
val = getattr(photosdb, attr)
|
||||||
|
print(f"{attr}:")
|
||||||
|
pprint.pprint(val)
|
||||||
|
except:
|
||||||
|
print(f"Did not find attribute {attr} in PhotosDB")
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@JSON_OPTION
|
@JSON_OPTION
|
||||||
@@ -451,7 +534,9 @@ def cli(ctx, db, json_, debug):
|
|||||||
def keywords(ctx, cli_obj, db, json_, photos_library):
|
def keywords(ctx, cli_obj, db, json_, photos_library):
|
||||||
""" Print out keywords found in the Photos library. """
|
""" Print out keywords found in the Photos library. """
|
||||||
|
|
||||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
# below needed for to make CliRunner work for testing
|
||||||
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
if db is None:
|
if db is None:
|
||||||
click.echo(cli.commands["keywords"].get_help(ctx), err=True)
|
click.echo(cli.commands["keywords"].get_help(ctx), err=True)
|
||||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||||
@@ -475,7 +560,9 @@ def keywords(ctx, cli_obj, db, json_, photos_library):
|
|||||||
def albums(ctx, cli_obj, db, json_, photos_library):
|
def albums(ctx, cli_obj, db, json_, photos_library):
|
||||||
""" Print out albums found in the Photos library. """
|
""" Print out albums found in the Photos library. """
|
||||||
|
|
||||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
# below needed for to make CliRunner work for testing
|
||||||
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
if db is None:
|
if db is None:
|
||||||
click.echo(cli.commands["albums"].get_help(ctx), err=True)
|
click.echo(cli.commands["albums"].get_help(ctx), err=True)
|
||||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||||
@@ -502,7 +589,9 @@ def albums(ctx, cli_obj, db, json_, photos_library):
|
|||||||
def persons(ctx, cli_obj, db, json_, photos_library):
|
def persons(ctx, cli_obj, db, json_, photos_library):
|
||||||
""" Print out persons (faces) found in the Photos library. """
|
""" Print out persons (faces) found in the Photos library. """
|
||||||
|
|
||||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
# below needed for to make CliRunner work for testing
|
||||||
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
if db is None:
|
if db is None:
|
||||||
click.echo(cli.commands["persons"].get_help(ctx), err=True)
|
click.echo(cli.commands["persons"].get_help(ctx), err=True)
|
||||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||||
@@ -517,6 +606,32 @@ def persons(ctx, cli_obj, db, json_, photos_library):
|
|||||||
click.echo(yaml.dump(persons, sort_keys=False))
|
click.echo(yaml.dump(persons, sort_keys=False))
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@DB_OPTION
|
||||||
|
@JSON_OPTION
|
||||||
|
@DB_ARGUMENT
|
||||||
|
@click.pass_obj
|
||||||
|
@click.pass_context
|
||||||
|
def labels(ctx, cli_obj, db, json_, photos_library):
|
||||||
|
""" Print out image classification labels found in the Photos library. """
|
||||||
|
|
||||||
|
# below needed for to make CliRunner work for testing
|
||||||
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
|
if db is None:
|
||||||
|
click.echo(cli.commands["labels"].get_help(ctx), err=True)
|
||||||
|
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||||
|
_list_libraries()
|
||||||
|
return
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||||
|
labels = {"labels": photosdb.labels_as_dict}
|
||||||
|
if json_ or cli_obj.json:
|
||||||
|
click.echo(json.dumps(labels))
|
||||||
|
else:
|
||||||
|
click.echo(yaml.dump(labels, sort_keys=False))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@JSON_OPTION
|
@JSON_OPTION
|
||||||
@@ -533,43 +648,40 @@ def info(ctx, cli_obj, db, json_, photos_library):
|
|||||||
_list_libraries()
|
_list_libraries()
|
||||||
return
|
return
|
||||||
|
|
||||||
pdb = osxphotos.PhotosDB(dbfile=db)
|
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||||
info = {}
|
info = {"database_path": photosdb.db_path, "database_version": photosdb.db_version}
|
||||||
info["database_path"] = pdb.db_path
|
photos = photosdb.photos()
|
||||||
info["database_version"] = pdb.db_version
|
|
||||||
|
|
||||||
photos = pdb.photos()
|
|
||||||
not_shared_photos = [p for p in photos if not p.shared]
|
not_shared_photos = [p for p in photos if not p.shared]
|
||||||
info["photo_count"] = len(not_shared_photos)
|
info["photo_count"] = len(not_shared_photos)
|
||||||
|
|
||||||
hidden = [p for p in photos if p.hidden]
|
hidden = [p for p in photos if p.hidden]
|
||||||
info["hidden_photo_count"] = len(hidden)
|
info["hidden_photo_count"] = len(hidden)
|
||||||
|
|
||||||
movies = pdb.photos(images=False, movies=True)
|
movies = photosdb.photos(images=False, movies=True)
|
||||||
not_shared_movies = [p for p in movies if not p.shared]
|
not_shared_movies = [p for p in movies if not p.shared]
|
||||||
info["movie_count"] = len(not_shared_movies)
|
info["movie_count"] = len(not_shared_movies)
|
||||||
|
|
||||||
if pdb.db_version > _PHOTOS_4_VERSION:
|
if photosdb.db_version > _PHOTOS_4_VERSION:
|
||||||
shared_photos = [p for p in photos if p.shared]
|
shared_photos = [p for p in photos if p.shared]
|
||||||
info["shared_photo_count"] = len(shared_photos)
|
info["shared_photo_count"] = len(shared_photos)
|
||||||
|
|
||||||
shared_movies = [p for p in movies if p.shared]
|
shared_movies = [p for p in movies if p.shared]
|
||||||
info["shared_movie_count"] = len(shared_movies)
|
info["shared_movie_count"] = len(shared_movies)
|
||||||
|
|
||||||
keywords = pdb.keywords_as_dict
|
keywords = photosdb.keywords_as_dict
|
||||||
info["keywords_count"] = len(keywords)
|
info["keywords_count"] = len(keywords)
|
||||||
info["keywords"] = keywords
|
info["keywords"] = keywords
|
||||||
|
|
||||||
albums = pdb.albums_as_dict
|
albums = photosdb.albums_as_dict
|
||||||
info["albums_count"] = len(albums)
|
info["albums_count"] = len(albums)
|
||||||
info["albums"] = albums
|
info["albums"] = albums
|
||||||
|
|
||||||
if pdb.db_version > _PHOTOS_4_VERSION:
|
if photosdb.db_version > _PHOTOS_4_VERSION:
|
||||||
albums_shared = pdb.albums_shared_as_dict
|
albums_shared = photosdb.albums_shared_as_dict
|
||||||
info["shared_albums_count"] = len(albums_shared)
|
info["shared_albums_count"] = len(albums_shared)
|
||||||
info["shared_albums"] = albums_shared
|
info["shared_albums"] = albums_shared
|
||||||
|
|
||||||
persons = pdb.persons_as_dict
|
persons = photosdb.persons_as_dict
|
||||||
|
|
||||||
info["persons_count"] = len(persons)
|
info["persons_count"] = len(persons)
|
||||||
info["persons"] = persons
|
info["persons"] = persons
|
||||||
@@ -646,8 +758,8 @@ def dump(ctx, cli_obj, db, json_, photos_library):
|
|||||||
_list_libraries()
|
_list_libraries()
|
||||||
return
|
return
|
||||||
|
|
||||||
pdb = osxphotos.PhotosDB(dbfile=db)
|
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||||
photos = pdb.photos(movies=True)
|
photos = photosdb.photos(movies=True)
|
||||||
print_photo_info(photos, json_ or cli_obj.json)
|
print_photo_info(photos, json_ or cli_obj.json)
|
||||||
|
|
||||||
|
|
||||||
@@ -788,6 +900,7 @@ def query(
|
|||||||
has_raw,
|
has_raw,
|
||||||
place,
|
place,
|
||||||
no_place,
|
no_place,
|
||||||
|
label,
|
||||||
):
|
):
|
||||||
""" Query the Photos database using 1 or more search options;
|
""" Query the Photos database using 1 or more search options;
|
||||||
if more than one option is provided, they are treated as "AND"
|
if more than one option is provided, they are treated as "AND"
|
||||||
@@ -808,6 +921,7 @@ def query(
|
|||||||
has_raw,
|
has_raw,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
|
label,
|
||||||
]
|
]
|
||||||
exclusive = [
|
exclusive = [
|
||||||
(favorite, not_favorite),
|
(favorite, not_favorite),
|
||||||
@@ -903,6 +1017,7 @@ def query(
|
|||||||
has_raw=has_raw,
|
has_raw=has_raw,
|
||||||
place=place,
|
place=place,
|
||||||
no_place=no_place,
|
no_place=no_place,
|
||||||
|
label=label,
|
||||||
)
|
)
|
||||||
|
|
||||||
# below needed for to make CliRunner work for testing
|
# below needed for to make CliRunner work for testing
|
||||||
@@ -1136,6 +1251,7 @@ def export(
|
|||||||
place,
|
place,
|
||||||
no_place,
|
no_place,
|
||||||
no_extended_attributes,
|
no_extended_attributes,
|
||||||
|
label,
|
||||||
):
|
):
|
||||||
""" Export photos from the Photos database.
|
""" Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1175,7 +1291,7 @@ def export(
|
|||||||
(export_as_hardlink, exiftool),
|
(export_as_hardlink, exiftool),
|
||||||
(any(place), no_place),
|
(any(place), no_place),
|
||||||
]
|
]
|
||||||
if any([all(bb) for bb in exclusive]):
|
if any(all(bb) for bb in exclusive):
|
||||||
click.echo("Incompatible export options", err=True)
|
click.echo("Incompatible export options", err=True)
|
||||||
click.echo(cli.commands["export"].get_help(ctx), err=True)
|
click.echo(cli.commands["export"].get_help(ctx), err=True)
|
||||||
return
|
return
|
||||||
@@ -1186,11 +1302,6 @@ def export(
|
|||||||
not x for x in [skip_edited, skip_bursts, skip_live, skip_raw]
|
not x for x in [skip_edited, skip_bursts, skip_live, skip_raw]
|
||||||
]
|
]
|
||||||
|
|
||||||
# though the command line option is current_name, internally all processing
|
|
||||||
# logic uses original_name which is the boolean inverse of current_name
|
|
||||||
# because the original code used --original-name as an option
|
|
||||||
original_name = not current_name
|
|
||||||
|
|
||||||
# verify exiftool installed an in path
|
# verify exiftool installed an in path
|
||||||
if exiftool:
|
if exiftool:
|
||||||
try:
|
try:
|
||||||
@@ -1219,13 +1330,32 @@ def export(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# open export database and assign copy/link/unlink functions
|
# open export database and assign copy/link/unlink functions
|
||||||
|
export_db_path = os.path.join(dest, OSXPHOTOS_EXPORT_DB)
|
||||||
|
|
||||||
|
# check that export isn't in the parent or child of a previously exported library
|
||||||
|
other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB)
|
||||||
|
if other_db_files:
|
||||||
|
click.echo(
|
||||||
|
"WARNING: found other export database files in this destination directory branch. "
|
||||||
|
+ "This likely means you are attempting to export files into a directory "
|
||||||
|
+ "that is either the parent or a child directory of a previous export. "
|
||||||
|
+ "Proceeding may cause your exported files to be overwritten.",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
click.echo(
|
||||||
|
f"You are exporting to {dest}, found {OSXPHOTOS_EXPORT_DB} files in:"
|
||||||
|
)
|
||||||
|
for other_db in other_db_files:
|
||||||
|
click.echo(f"{other_db}")
|
||||||
|
click.confirm("Do you want to continue?", abort=True)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
export_db = ExportDBInMemory(os.path.join(dest, OSXPHOTOS_EXPORT_DB))
|
export_db = ExportDBInMemory(export_db_path)
|
||||||
# echo = functools.partial(click.echo, err=True)
|
# echo = functools.partial(click.echo, err=True)
|
||||||
# fileutil = FileUtilNoOp(verbose=echo)
|
# fileutil = FileUtilNoOp(verbose=echo)
|
||||||
fileutil = FileUtilNoOp
|
fileutil = FileUtilNoOp
|
||||||
else:
|
else:
|
||||||
export_db = ExportDB(os.path.join(dest, OSXPHOTOS_EXPORT_DB))
|
export_db = ExportDB(export_db_path)
|
||||||
fileutil = FileUtil
|
fileutil = FileUtil
|
||||||
|
|
||||||
photos = _query(
|
photos = _query(
|
||||||
@@ -1280,13 +1410,10 @@ def export(
|
|||||||
has_raw=has_raw,
|
has_raw=has_raw,
|
||||||
place=place,
|
place=place,
|
||||||
no_place=no_place,
|
no_place=no_place,
|
||||||
|
label=label,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported = []
|
results_exported = []
|
||||||
results_new = []
|
|
||||||
results_updated = []
|
|
||||||
results_skipped = []
|
|
||||||
results_exif_updated = []
|
|
||||||
if photos:
|
if photos:
|
||||||
if export_bursts:
|
if export_bursts:
|
||||||
# add the burst_photos to the export set
|
# add the burst_photos to the export set
|
||||||
@@ -1300,7 +1427,50 @@ def export(
|
|||||||
photo_str = "photos" if num_photos > 1 else "photo"
|
photo_str = "photos" if num_photos > 1 else "photo"
|
||||||
click.echo(f"Exporting {num_photos} {photo_str} to {dest}...")
|
click.echo(f"Exporting {num_photos} {photo_str} to {dest}...")
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
if not verbose_:
|
# though the command line option is current_name, internally all processing
|
||||||
|
# logic uses original_name which is the boolean inverse of current_name
|
||||||
|
# because the original code used --original-name as an option
|
||||||
|
original_name = not current_name
|
||||||
|
|
||||||
|
results_new = []
|
||||||
|
results_updated = []
|
||||||
|
results_skipped = []
|
||||||
|
results_exif_updated = []
|
||||||
|
if verbose_:
|
||||||
|
for p in photos:
|
||||||
|
results = export_photo(
|
||||||
|
photo=p,
|
||||||
|
dest=dest,
|
||||||
|
verbose_=verbose_,
|
||||||
|
export_by_date=export_by_date,
|
||||||
|
sidecar=sidecar,
|
||||||
|
update=update,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
|
overwrite=overwrite,
|
||||||
|
export_edited=export_edited,
|
||||||
|
original_name=original_name,
|
||||||
|
export_live=export_live,
|
||||||
|
download_missing=download_missing,
|
||||||
|
exiftool=exiftool,
|
||||||
|
directory=directory,
|
||||||
|
filename_template=filename_template,
|
||||||
|
no_extended_attributes=no_extended_attributes,
|
||||||
|
export_raw=export_raw,
|
||||||
|
album_keyword=album_keyword,
|
||||||
|
person_keyword=person_keyword,
|
||||||
|
keyword_template=keyword_template,
|
||||||
|
export_db=export_db,
|
||||||
|
fileutil=fileutil,
|
||||||
|
dry_run=dry_run,
|
||||||
|
edited_suffix=edited_suffix,
|
||||||
|
)
|
||||||
|
results_exported.extend(results.exported)
|
||||||
|
results_new.extend(results.new)
|
||||||
|
results_updated.extend(results.updated)
|
||||||
|
results_skipped.extend(results.skipped)
|
||||||
|
results_exif_updated.extend(results.exif_updated)
|
||||||
|
|
||||||
|
else:
|
||||||
# show progress bar
|
# show progress bar
|
||||||
with click.progressbar(photos) as bar:
|
with click.progressbar(photos) as bar:
|
||||||
for p in bar:
|
for p in bar:
|
||||||
@@ -1335,47 +1505,9 @@ def export(
|
|||||||
results_updated.extend(results.updated)
|
results_updated.extend(results.updated)
|
||||||
results_skipped.extend(results.skipped)
|
results_skipped.extend(results.skipped)
|
||||||
results_exif_updated.extend(results.exif_updated)
|
results_exif_updated.extend(results.exif_updated)
|
||||||
else:
|
|
||||||
for p in photos:
|
|
||||||
results = export_photo(
|
|
||||||
photo=p,
|
|
||||||
dest=dest,
|
|
||||||
verbose_=verbose_,
|
|
||||||
export_by_date=export_by_date,
|
|
||||||
sidecar=sidecar,
|
|
||||||
update=update,
|
|
||||||
export_as_hardlink=export_as_hardlink,
|
|
||||||
overwrite=overwrite,
|
|
||||||
export_edited=export_edited,
|
|
||||||
original_name=original_name,
|
|
||||||
export_live=export_live,
|
|
||||||
download_missing=download_missing,
|
|
||||||
exiftool=exiftool,
|
|
||||||
directory=directory,
|
|
||||||
filename_template=filename_template,
|
|
||||||
no_extended_attributes=no_extended_attributes,
|
|
||||||
export_raw=export_raw,
|
|
||||||
album_keyword=album_keyword,
|
|
||||||
person_keyword=person_keyword,
|
|
||||||
keyword_template=keyword_template,
|
|
||||||
export_db=export_db,
|
|
||||||
fileutil=fileutil,
|
|
||||||
dry_run=dry_run,
|
|
||||||
edited_suffix=edited_suffix,
|
|
||||||
)
|
|
||||||
results_exported.extend(results.exported)
|
|
||||||
results_new.extend(results.new)
|
|
||||||
results_updated.extend(results.updated)
|
|
||||||
results_skipped.extend(results.skipped)
|
|
||||||
results_exif_updated.extend(results.exif_updated)
|
|
||||||
|
|
||||||
stop_time = time.perf_counter()
|
stop_time = time.perf_counter()
|
||||||
# print summary results
|
# print summary results
|
||||||
if not update:
|
if update:
|
||||||
photo_str = "photos" if len(results_exported) != 1 else "photo"
|
|
||||||
click.echo(f"Exported: {len(results_exported)} {photo_str}")
|
|
||||||
click.echo(f"Elapsed time: {stop_time-start_time} seconds")
|
|
||||||
else:
|
|
||||||
photo_str_new = "photos" if len(results_new) != 1 else "photo"
|
photo_str_new = "photos" if len(results_new) != 1 else "photo"
|
||||||
photo_str_updated = "photos" if len(results_new) != 1 else "photo"
|
photo_str_updated = "photos" if len(results_new) != 1 else "photo"
|
||||||
photo_str_skipped = "photos" if len(results_skipped) != 1 else "photo"
|
photo_str_skipped = "photos" if len(results_skipped) != 1 else "photo"
|
||||||
@@ -1388,8 +1520,10 @@ def export(
|
|||||||
+ f"skipped: {len(results_skipped)} {photo_str_skipped}, "
|
+ f"skipped: {len(results_skipped)} {photo_str_skipped}, "
|
||||||
+ f"updated EXIF data: {len(results_exif_updated)} {photo_str_exif_updated}"
|
+ f"updated EXIF data: {len(results_exif_updated)} {photo_str_exif_updated}"
|
||||||
)
|
)
|
||||||
click.echo(f"Elapsed time: {stop_time-start_time} seconds")
|
else:
|
||||||
|
photo_str = "photos" if len(results_exported) != 1 else "photo"
|
||||||
|
click.echo(f"Exported: {len(results_exported)} {photo_str}")
|
||||||
|
click.echo(f"Elapsed time: {(stop_time-start_time):.3f} seconds")
|
||||||
else:
|
else:
|
||||||
click.echo("Did not find any photos to export")
|
click.echo("Did not find any photos to export")
|
||||||
|
|
||||||
@@ -1412,8 +1546,8 @@ def help(ctx, topic, **kw):
|
|||||||
|
|
||||||
|
|
||||||
def print_photo_info(photos, json=False):
|
def print_photo_info(photos, json=False):
|
||||||
|
dump = []
|
||||||
if json:
|
if json:
|
||||||
dump = []
|
|
||||||
for p in photos:
|
for p in photos:
|
||||||
dump.append(p.json())
|
dump.append(p.json())
|
||||||
click.echo(f"[{', '.join(dump)}]")
|
click.echo(f"[{', '.join(dump)}]")
|
||||||
@@ -1422,7 +1556,6 @@ def print_photo_info(photos, json=False):
|
|||||||
csv_writer = csv.writer(
|
csv_writer = csv.writer(
|
||||||
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
|
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
|
||||||
)
|
)
|
||||||
dump = []
|
|
||||||
# add headers
|
# add headers
|
||||||
dump.append(
|
dump.append(
|
||||||
[
|
[
|
||||||
@@ -1566,6 +1699,7 @@ def _query(
|
|||||||
has_raw=None,
|
has_raw=None,
|
||||||
place=None,
|
place=None,
|
||||||
no_place=None,
|
no_place=None,
|
||||||
|
label=None,
|
||||||
):
|
):
|
||||||
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
||||||
used by query and export commands
|
used by query and export commands
|
||||||
@@ -1574,16 +1708,21 @@ def _query(
|
|||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||||
photos = photosdb.photos(
|
photos = photosdb.photos(
|
||||||
keywords=keyword,
|
uuid=uuid, images=isphoto, movies=ismovie, from_date=from_date, to_date=to_date
|
||||||
persons=person,
|
|
||||||
albums=album,
|
|
||||||
uuid=uuid,
|
|
||||||
images=isphoto,
|
|
||||||
movies=ismovie,
|
|
||||||
from_date=from_date,
|
|
||||||
to_date=to_date,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if album:
|
||||||
|
photos = get_photos_by_attribute(photos, "albums", album, ignore_case)
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
photos = get_photos_by_attribute(photos, "keywords", keyword, ignore_case)
|
||||||
|
|
||||||
|
if person:
|
||||||
|
photos = get_photos_by_attribute(photos, "persons", person, ignore_case)
|
||||||
|
|
||||||
|
if label:
|
||||||
|
photos = get_photos_by_attribute(photos, "labels", label, ignore_case)
|
||||||
|
|
||||||
if folder:
|
if folder:
|
||||||
# search for photos in an album in folder
|
# search for photos in an album in folder
|
||||||
# finds photos that have albums whose top level folder matches folder
|
# finds photos that have albums whose top level folder matches folder
|
||||||
@@ -1758,6 +1897,34 @@ def _query(
|
|||||||
return photos
|
return photos
|
||||||
|
|
||||||
|
|
||||||
|
def get_photos_by_attribute(photos, attribute, values, ignore_case):
|
||||||
|
"""Search for photos based on values being in PhotoInfo.attribute
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photos: a list of PhotoInfo objects
|
||||||
|
attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc)
|
||||||
|
values: list of values to search in property
|
||||||
|
ignore_case: ignore case when searching
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of PhotoInfo objects matching search criteria
|
||||||
|
"""
|
||||||
|
photos_search = []
|
||||||
|
if ignore_case:
|
||||||
|
# case-insensitive
|
||||||
|
for x in values:
|
||||||
|
x = x.lower()
|
||||||
|
photos_search.extend(
|
||||||
|
p
|
||||||
|
for p in photos
|
||||||
|
if x in [attr.lower() for attr in getattr(p, attribute)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for x in values:
|
||||||
|
photos_search.extend(p for p in photos if x in getattr(p, attribute))
|
||||||
|
return photos_search
|
||||||
|
|
||||||
|
|
||||||
def export_photo(
|
def export_photo(
|
||||||
photo=None,
|
photo=None,
|
||||||
dest=None,
|
dest=None,
|
||||||
@@ -1990,10 +2157,7 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
|||||||
)
|
)
|
||||||
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
||||||
else:
|
else:
|
||||||
if original_name:
|
filenames = [photo.original_filename] if original_name else [photo.filename]
|
||||||
filenames = [photo.original_filename]
|
|
||||||
else:
|
|
||||||
filenames = [photo.filename]
|
|
||||||
return filenames
|
return filenames
|
||||||
|
|
||||||
|
|
||||||
@@ -2019,13 +2183,18 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
|||||||
dest_path = os.path.join(
|
dest_path = os.path.join(
|
||||||
dest, date_created.year, date_created.mm, date_created.dd
|
dest, date_created.year, date_created.mm, date_created.dd
|
||||||
)
|
)
|
||||||
if not dry_run and not os.path.isdir(dest_path):
|
if not (dry_run or os.path.isdir(dest_path)):
|
||||||
os.makedirs(dest_path)
|
os.makedirs(dest_path)
|
||||||
dest_paths = [dest_path]
|
dest_paths = [dest_path]
|
||||||
elif directory:
|
elif directory:
|
||||||
# got a directory template, render it and check results are valid
|
# got a directory template, render it and check results are valid
|
||||||
dirnames, unmatched = photo.render_template(directory)
|
dirnames, unmatched = photo.render_template(directory)
|
||||||
if not dirnames or unmatched:
|
if not dirnames:
|
||||||
|
raise click.BadOptionUsage(
|
||||||
|
"directory",
|
||||||
|
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
||||||
|
)
|
||||||
|
elif unmatched:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"directory",
|
"directory",
|
||||||
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
||||||
@@ -2044,5 +2213,42 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
|||||||
return dest_paths
|
return dest_paths
|
||||||
|
|
||||||
|
|
||||||
|
def find_files_in_branch(pathname, filename):
|
||||||
|
""" Search a directory branch to find file(s) named filename
|
||||||
|
The branch searched includes all folders below pathname and
|
||||||
|
the parent tree of pathname but not pathname itself.
|
||||||
|
|
||||||
|
e.g. find filename in children folders and parent folders
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pathname: str, full path of directory to search
|
||||||
|
filename: str, filename to search for
|
||||||
|
|
||||||
|
Returns: list of full paths to any matching files
|
||||||
|
"""
|
||||||
|
|
||||||
|
pathname = pathlib.Path(pathname).resolve()
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# walk down the tree
|
||||||
|
for root, directories, filenames in os.walk(pathname):
|
||||||
|
# for directory in directories:
|
||||||
|
# print(os.path.join(root, directory))
|
||||||
|
for fname in filenames:
|
||||||
|
if fname == filename and pathlib.Path(root) != pathname:
|
||||||
|
files.append(os.path.join(root, fname))
|
||||||
|
|
||||||
|
# walk up the tree
|
||||||
|
path = pathlib.Path(pathname)
|
||||||
|
for root in path.parents:
|
||||||
|
filenames = os.listdir(root)
|
||||||
|
for fname in filenames:
|
||||||
|
filepath = os.path.join(root, fname)
|
||||||
|
if fname == filename and os.path.isfile(filepath):
|
||||||
|
files.append(os.path.join(root, fname))
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli() # pylint: disable=no-value-for-parameter
|
cli() # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
@@ -445,10 +445,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
dt = datetime.datetime.utcnow().isoformat()
|
dt = datetime.datetime.utcnow().isoformat()
|
||||||
python_path = sys.executable
|
python_path = sys.executable
|
||||||
cmd = sys.argv[0]
|
cmd = sys.argv[0]
|
||||||
if len(sys.argv) > 1:
|
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
|
||||||
args = " ".join(sys.argv[1:])
|
|
||||||
else:
|
|
||||||
args = ""
|
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
conn = self._conn
|
conn = self._conn
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.29.9"
|
__version__ = "0.29.26"
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ class AlbumInfo:
|
|||||||
try:
|
try:
|
||||||
return self._folder_names
|
return self._folder_names
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
||||||
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
|
||||||
else:
|
|
||||||
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
|
||||||
return self._folder_names
|
return self._folder_names
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -12,53 +12,59 @@ class DateTimeFormatter:
|
|||||||
@property
|
@property
|
||||||
def date(self):
|
def date(self):
|
||||||
""" ISO date in form 2020-03-22 """
|
""" ISO date in form 2020-03-22 """
|
||||||
date = self.dt.date().isoformat()
|
return self.dt.date().isoformat()
|
||||||
return date
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year(self):
|
def year(self):
|
||||||
""" 4 digit year """
|
""" 4 digit year """
|
||||||
year = f"{self.dt.year}"
|
return f"{self.dt.year}"
|
||||||
return year
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def yy(self):
|
def yy(self):
|
||||||
""" 2 digit year """
|
""" 2 digit year """
|
||||||
yy = f"{self.dt.strftime('%y')}"
|
return f"{self.dt.strftime('%y')}"
|
||||||
return yy
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mm(self):
|
def mm(self):
|
||||||
""" 2 digit month """
|
""" 2 digit month """
|
||||||
mm = f"{self.dt.strftime('%m')}"
|
return f"{self.dt.strftime('%m')}"
|
||||||
return mm
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def month(self):
|
def month(self):
|
||||||
""" Month as locale's full name """
|
""" Month as locale's full name """
|
||||||
month = f"{self.dt.strftime('%B')}"
|
return f"{self.dt.strftime('%B')}"
|
||||||
return month
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mon(self):
|
def mon(self):
|
||||||
""" Month as locale's abbreviated name """
|
""" Month as locale's abbreviated name """
|
||||||
mon = f"{self.dt.strftime('%b')}"
|
return f"{self.dt.strftime('%b')}"
|
||||||
return mon
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dd(self):
|
def dd(self):
|
||||||
""" 2-digit day of the month """
|
""" 2-digit day of the month """
|
||||||
dd = f"{self.dt.strftime('%d')}"
|
return f"{self.dt.strftime('%d')}"
|
||||||
return dd
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dow(self):
|
def dow(self):
|
||||||
""" Day of week as locale's name """
|
""" Day of week as locale's name """
|
||||||
dow = f"{self.dt.strftime('%A')}"
|
return f"{self.dt.strftime('%A')}"
|
||||||
return dow
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def doy(self):
|
def doy(self):
|
||||||
""" Julian day of year starting from 001 """
|
""" Julian day of year starting from 001 """
|
||||||
doy = f"{self.dt.strftime('%j')}"
|
return f"{self.dt.strftime('%j')}"
|
||||||
return doy
|
|
||||||
|
@property
|
||||||
|
def hour(self):
|
||||||
|
""" 2-digit hour """
|
||||||
|
return f"{self.dt.strftime('%H')}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
""" 2-digit minute """
|
||||||
|
return f"{self.dt.strftime('%M')}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sec(self):
|
||||||
|
""" 2-digit second """
|
||||||
|
return f"{self.dt.strftime('%S')}"
|
||||||
|
|||||||
@@ -59,11 +59,7 @@ class _ExifToolProc:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if exiftool:
|
self._exiftool = exiftool if exiftool else get_exiftool_path()
|
||||||
self._exiftool = exiftool
|
|
||||||
else:
|
|
||||||
self._exiftool = get_exiftool_path()
|
|
||||||
|
|
||||||
self._process_running = False
|
self._process_running = False
|
||||||
self._start_proc()
|
self._start_proc()
|
||||||
|
|
||||||
@@ -156,8 +152,7 @@ class ExifTool:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
command = []
|
command = [f"-{tag}={value}"]
|
||||||
command.append(f"-{tag}={value}")
|
|
||||||
if self.overwrite:
|
if self.overwrite:
|
||||||
command.append("-overwrite_original")
|
command.append("-overwrite_original")
|
||||||
self.run_commands(*command)
|
self.run_commands(*command)
|
||||||
@@ -193,7 +188,7 @@ class ExifTool:
|
|||||||
no_file: (bool) do not pass the filename to exiftool (default=False)
|
no_file: (bool) do not pass the filename to exiftool (default=False)
|
||||||
by default, all commands will be run against self.file
|
by default, all commands will be run against self.file
|
||||||
use no_file=True to run a command without passing the filename """
|
use no_file=True to run a command without passing the filename """
|
||||||
if not hasattr(self, "_process") or not self._process:
|
if not (hasattr(self, "_process") and self._process):
|
||||||
raise ValueError("exiftool process is not running")
|
raise ValueError("exiftool process is not running")
|
||||||
|
|
||||||
if not commands:
|
if not commands:
|
||||||
@@ -245,8 +240,7 @@ class ExifTool:
|
|||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
""" returns JSON string containing all EXIF tags and values from exiftool """
|
""" returns JSON string containing all EXIF tags and values from exiftool """
|
||||||
json_str = self.run_commands("-json")
|
return self.run_commands("-json")
|
||||||
return json_str
|
|
||||||
|
|
||||||
def _read_exif(self):
|
def _read_exif(self):
|
||||||
""" read exif data from file """
|
""" read exif data from file """
|
||||||
@@ -254,5 +248,4 @@ class ExifTool:
|
|||||||
self.data = {k: v for k, v in data.items()}
|
self.data = {k: v for k, v in data.items()}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
str_ = f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
|
return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
|
||||||
return str_
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from abc import ABC, abstractmethod
|
|||||||
|
|
||||||
class FileUtilABC(ABC):
|
class FileUtilABC(ABC):
|
||||||
""" Abstract base class for FileUtil """
|
""" Abstract base class for FileUtil """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def hardlink(cls, src, dest):
|
def hardlink(cls, src, dest):
|
||||||
@@ -39,6 +40,7 @@ class FileUtilABC(ABC):
|
|||||||
|
|
||||||
class FileUtilMacOS(FileUtilABC):
|
class FileUtilMacOS(FileUtilABC):
|
||||||
""" Various file utilities """
|
""" Various file utilities """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def hardlink(cls, src, dest):
|
def hardlink(cls, src, dest):
|
||||||
""" Hardlinks a file from src path to dest path
|
""" Hardlinks a file from src path to dest path
|
||||||
@@ -119,9 +121,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
||||||
return False
|
return False
|
||||||
if s1 == s2:
|
return s1 == s2
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def file_sig(cls, f1):
|
def file_sig(cls, f1):
|
||||||
@@ -135,14 +135,17 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
class FileUtil(FileUtilMacOS):
|
class FileUtil(FileUtilMacOS):
|
||||||
""" Various file utilities """
|
""" Various file utilities """
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileUtilNoOp(FileUtil):
|
class FileUtilNoOp(FileUtil):
|
||||||
""" No-Op implementation of FileUtil for testing / dry-run mode
|
""" No-Op implementation of FileUtil for testing / dry-run mode
|
||||||
all methods with exception of cmp_sig and file_cmp are no-op
|
all methods with exception of cmp_sig and file_cmp are no-op
|
||||||
cmp_sig functions as FileUtil.cmp_sig does
|
cmp_sig functions as FileUtil.cmp_sig does
|
||||||
file_cmp returns mock data
|
file_cmp returns mock data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def noop(*args):
|
def noop(*args):
|
||||||
pass
|
pass
|
||||||
@@ -155,7 +158,7 @@ class FileUtilNoOp(FileUtil):
|
|||||||
cls.verbose = verbose
|
cls.verbose = verbose
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"verbose {verbose} not callable")
|
raise ValueError(f"verbose {verbose} not callable")
|
||||||
return super(FileUtilNoOp, cls).__new__(cls)
|
return super(FileUtilNoOp, cls).__new__(cls)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def hardlink(cls, src, dest):
|
def hardlink(cls, src, dest):
|
||||||
@@ -164,7 +167,7 @@ class FileUtilNoOp(FileUtil):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, src, dest, norsrc=False):
|
def copy(cls, src, dest, norsrc=False):
|
||||||
cls.verbose(f"copy: {src} {dest}")
|
cls.verbose(f"copy: {src} {dest}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unlink(cls, dest):
|
def unlink(cls, dest):
|
||||||
cls.verbose(f"unlink: {dest}")
|
cls.verbose(f"unlink: {dest}")
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
|||||||
|
|
||||||
from ._photoinfo_exifinfo import ExifInfo
|
from ._photoinfo_exifinfo import ExifInfo
|
||||||
from ._photoinfo_export import ExportResults
|
from ._photoinfo_export import ExportResults
|
||||||
|
from ._photoinfo_scoreinfo import ScoreInfo
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
|
|||||||
@@ -156,6 +156,46 @@ def _export_photo_uuid_applescript(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# _check_export_suffix is not a class method, don't import this into PhotoInfo
|
||||||
|
def _check_export_suffix(src, dest, edited):
|
||||||
|
"""Helper function for exporting photos to check file extensions of destination path.
|
||||||
|
|
||||||
|
Checks that dst file extension is appropriate for the src.
|
||||||
|
If edited=True, will use src file extension of ".jpeg" if None provided for src.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src: path to source file or None.
|
||||||
|
dest: path to destination file.
|
||||||
|
edited: set to True if exporting an edited photo.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if src and dest extensions are OK, else False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError if edited is False and src is None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# check extension of destination
|
||||||
|
if src is not None:
|
||||||
|
# use suffix from edited file
|
||||||
|
actual_suffix = pathlib.Path(src).suffix
|
||||||
|
elif edited:
|
||||||
|
# use .jpeg as that's probably correct
|
||||||
|
actual_suffix = ".jpeg"
|
||||||
|
else:
|
||||||
|
raise ValueError("src must not be None if edited=False")
|
||||||
|
|
||||||
|
# Photo's often converts .JPG to .jpeg or .tif to .tiff on import
|
||||||
|
dest_ext = dest.suffix.lower()
|
||||||
|
actual_ext = actual_suffix.lower()
|
||||||
|
suffixes = sorted([dest_ext, actual_ext])
|
||||||
|
return (
|
||||||
|
dest_ext == actual_ext
|
||||||
|
or suffixes == [".jpeg", ".jpg"]
|
||||||
|
or suffixes == [".tif", ".tiff"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def export(
|
def export(
|
||||||
self,
|
self,
|
||||||
dest,
|
dest,
|
||||||
@@ -182,8 +222,11 @@ def export(
|
|||||||
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
||||||
For example, if photo is .CR2 file, edited image may be .jpeg.
|
For example, if photo is .CR2 file, edited image may be .jpeg.
|
||||||
If you provide an extension different than what the actual file is,
|
If you provide an extension different than what the actual file is,
|
||||||
export will print a warning but will happily export the photo using the
|
export will print a warning but will export the photo using the
|
||||||
incorrect file extension. e.g. to get the extension of the edited photo,
|
incorrect file extension (unless use_photos_export is true, in which case export will
|
||||||
|
use the extension provided by Photos upon export; in this case, an incorrect extension is
|
||||||
|
silently ignored).
|
||||||
|
e.g. to get the extension of the edited photo,
|
||||||
reference PhotoInfo.path_edited
|
reference PhotoInfo.path_edited
|
||||||
edited: (boolean, default=False); if True will export the edited version of the photo
|
edited: (boolean, default=False); if True will export the edited version of the photo
|
||||||
(or raise exception if no edited version)
|
(or raise exception if no edited version)
|
||||||
@@ -260,13 +303,16 @@ def export2(
|
|||||||
dry_run=False,
|
dry_run=False,
|
||||||
):
|
):
|
||||||
""" export photo, like export but with update and dry_run options
|
""" export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path (or exception raised)
|
dest: must be valid destination path or exception raised
|
||||||
filename: (optional): name of exported picture; if not provided, will use current filename
|
filename: (optional): name of exported picture; if not provided, will use current filename
|
||||||
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
||||||
For example, if photo is .CR2 file, edited image may be .jpeg.
|
For example, if photo is .CR2 file, edited image may be .jpeg.
|
||||||
If you provide an extension different than what the actual file is,
|
If you provide an extension different than what the actual file is,
|
||||||
export will print a warning but will happily export the photo using the
|
export will print a warning but will export the photo using the
|
||||||
incorrect file extension. e.g. to get the extension of the edited photo,
|
incorrect file extension (unless use_photos_export is true, in which case export will
|
||||||
|
use the extension provided by Photos upon export; in this case, an incorrect extension is
|
||||||
|
silently ignored).
|
||||||
|
e.g. to get the extension of the edited photo,
|
||||||
reference PhotoInfo.path_edited
|
reference PhotoInfo.path_edited
|
||||||
edited: (boolean, default=False); if True will export the edited version of the photo
|
edited: (boolean, default=False); if True will export the edited version of the photo
|
||||||
(or raise exception if no edited version)
|
(or raise exception if no edited version)
|
||||||
@@ -370,27 +416,6 @@ def export2(
|
|||||||
fname = pathlib.Path(fname)
|
fname = pathlib.Path(fname)
|
||||||
dest = dest / fname
|
dest = dest / fname
|
||||||
|
|
||||||
# check extension of destination
|
|
||||||
if edited and self.path_edited is not None:
|
|
||||||
# use suffix from edited file
|
|
||||||
actual_suffix = pathlib.Path(self.path_edited).suffix
|
|
||||||
elif edited:
|
|
||||||
# use .jpeg as that's probably correct
|
|
||||||
# if edited and path_edited is None, will raise FileNotFoundError below
|
|
||||||
# unless use_photos_export is True
|
|
||||||
actual_suffix = ".jpeg"
|
|
||||||
else:
|
|
||||||
# use suffix from the non-edited file
|
|
||||||
actual_suffix = pathlib.Path(self.filename).suffix
|
|
||||||
|
|
||||||
# warn if suffixes don't match but ignore .JPG / .jpeg as
|
|
||||||
# Photo's often converts .JPG to .jpeg
|
|
||||||
suffixes = sorted([x.lower() for x in [dest.suffix, actual_suffix]])
|
|
||||||
if dest.suffix.lower() != actual_suffix.lower() and suffixes != [".jpeg", ".jpg"]:
|
|
||||||
logging.warning(
|
|
||||||
f"Invalid destination suffix: {dest.suffix}, should be {actual_suffix}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# check to see if file exists and if so, add (1), (2), etc until we find one that works
|
# check to see if file exists and if so, add (1), (2), etc until we find one that works
|
||||||
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
|
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
|
||||||
# e.g. exporting sidecar for file1.png and file1.jpeg
|
# e.g. exporting sidecar for file1.png and file1.jpeg
|
||||||
@@ -438,6 +463,13 @@ def export2(
|
|||||||
if not os.path.isfile(src):
|
if not os.path.isfile(src):
|
||||||
raise FileNotFoundError(f"{src} does not appear to exist")
|
raise FileNotFoundError(f"{src} does not appear to exist")
|
||||||
|
|
||||||
|
if not _check_export_suffix(src, dest, edited):
|
||||||
|
logging.warning(
|
||||||
|
f"Invalid destination suffix: {dest.suffix} for {self.path}, "
|
||||||
|
+ f"edited={edited}, path_edited={self.path_edited}, "
|
||||||
|
+ f"original_filename={self.original_filename}, filename={self.filename}"
|
||||||
|
)
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"exporting {src} to {dest}, overwrite={overwrite}, increment={increment}, dest exists: {dest.exists()}"
|
f"exporting {src} to {dest}, overwrite={overwrite}, increment={increment}, dest exists: {dest.exists()}"
|
||||||
)
|
)
|
||||||
@@ -757,6 +789,8 @@ def _export_photo(
|
|||||||
action depending on update, overwrite
|
action depending on update, overwrite
|
||||||
Assumes destination is the right destination (e.g. UUID matches)
|
Assumes destination is the right destination (e.g. UUID matches)
|
||||||
sets UUID and JSON info foo exported file using set_uuid_for_file, set_inf_for_uuido
|
sets UUID and JSON info foo exported file using set_uuid_for_file, set_inf_for_uuido
|
||||||
|
|
||||||
|
Args:
|
||||||
src: src path (string)
|
src: src path (string)
|
||||||
dest: dest path (pathlib.Path)
|
dest: dest path (pathlib.Path)
|
||||||
update: bool
|
update: bool
|
||||||
@@ -766,7 +800,9 @@ def _export_photo(
|
|||||||
export_as_hardlink: bool
|
export_as_hardlink: bool
|
||||||
exiftool: bool
|
exiftool: bool
|
||||||
fileutil: FileUtil class that conforms to fileutil.FileUtilABC
|
fileutil: FileUtil class that conforms to fileutil.FileUtilABC
|
||||||
Returns: ExportResults
|
|
||||||
|
Returns:
|
||||||
|
ExportResults
|
||||||
"""
|
"""
|
||||||
|
|
||||||
exported_files = []
|
exported_files = []
|
||||||
@@ -1153,7 +1189,7 @@ def _xmp_sidecar(
|
|||||||
def _write_sidecar(self, filename, sidecar_str):
|
def _write_sidecar(self, filename, sidecar_str):
|
||||||
""" write sidecar_str to filename
|
""" write sidecar_str to filename
|
||||||
used for exporting sidecar info """
|
used for exporting sidecar info """
|
||||||
if not filename and not sidecar_str:
|
if not (filename or sidecar_str):
|
||||||
raise (
|
raise (
|
||||||
ValueError(
|
ValueError(
|
||||||
f"filename {filename} and sidecar_str {sidecar_str} must not be None"
|
f"filename {filename} and sidecar_str {sidecar_str} must not be None"
|
||||||
|
|||||||
119
osxphotos/photoinfo/_photoinfo_scoreinfo.py
Normal file
119
osxphotos/photoinfo/_photoinfo_scoreinfo.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
""" PhotoInfo methods to expose computed score info from the library """
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .._constants import _PHOTOS_4_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScoreInfo:
|
||||||
|
""" Computed photo score info associated with a photo from the Photos library """
|
||||||
|
|
||||||
|
overall: float
|
||||||
|
curation: float
|
||||||
|
promotion: float
|
||||||
|
highlight_visibility: float
|
||||||
|
behavioral: float
|
||||||
|
failure: float
|
||||||
|
harmonious_color: float
|
||||||
|
immersiveness: float
|
||||||
|
interaction: float
|
||||||
|
interesting_subject: float
|
||||||
|
intrusive_object_presence: float
|
||||||
|
lively_color: float
|
||||||
|
low_light: float
|
||||||
|
noise: float
|
||||||
|
pleasant_camera_tilt: float
|
||||||
|
pleasant_composition: float
|
||||||
|
pleasant_lighting: float
|
||||||
|
pleasant_pattern: float
|
||||||
|
pleasant_perspective: float
|
||||||
|
pleasant_post_processing: float
|
||||||
|
pleasant_reflection: float
|
||||||
|
pleasant_symmetry: float
|
||||||
|
sharply_focused_subject: float
|
||||||
|
tastefully_blurred: float
|
||||||
|
well_chosen_subject: float
|
||||||
|
well_framed_subject: float
|
||||||
|
well_timed_shot: float
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self):
|
||||||
|
""" Computed score information for a photo
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ScoreInfo instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
logging.debug(f"score not implemented for this database version")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=scores["overall_aesthetic"],
|
||||||
|
curation=scores["curation"],
|
||||||
|
promotion=scores["promotion"],
|
||||||
|
highlight_visibility=scores["highlight_visibility"],
|
||||||
|
behavioral=scores["behavioral"],
|
||||||
|
failure=scores["failure"],
|
||||||
|
harmonious_color=scores["harmonious_color"],
|
||||||
|
immersiveness=scores["immersiveness"],
|
||||||
|
interaction=scores["interaction"],
|
||||||
|
interesting_subject=scores["interesting_subject"],
|
||||||
|
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||||
|
lively_color=scores["lively_color"],
|
||||||
|
low_light=scores["low_light"],
|
||||||
|
noise=scores["noise"],
|
||||||
|
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||||
|
pleasant_composition=scores["pleasant_composition"],
|
||||||
|
pleasant_lighting=scores["pleasant_lighting"],
|
||||||
|
pleasant_pattern=scores["pleasant_pattern"],
|
||||||
|
pleasant_perspective=scores["pleasant_perspective"],
|
||||||
|
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||||
|
pleasant_reflection=scores["pleasant_reflection"],
|
||||||
|
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||||
|
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||||
|
tastefully_blurred=scores["tastefully_blurred"],
|
||||||
|
well_chosen_subject=scores["well_chosen_subject"],
|
||||||
|
well_framed_subject=scores["well_framed_subject"],
|
||||||
|
well_timed_shot=scores["well_timed_shot"],
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
|
except KeyError:
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=0.0,
|
||||||
|
curation=0.0,
|
||||||
|
promotion=0.0,
|
||||||
|
highlight_visibility=0.0,
|
||||||
|
behavioral=0.0,
|
||||||
|
failure=0.0,
|
||||||
|
harmonious_color=0.0,
|
||||||
|
immersiveness=0.0,
|
||||||
|
interaction=0.0,
|
||||||
|
interesting_subject=0.0,
|
||||||
|
intrusive_object_presence=0.0,
|
||||||
|
lively_color=0.0,
|
||||||
|
low_light=0.0,
|
||||||
|
noise=0.0,
|
||||||
|
pleasant_camera_tilt=0.0,
|
||||||
|
pleasant_composition=0.0,
|
||||||
|
pleasant_lighting=0.0,
|
||||||
|
pleasant_pattern=0.0,
|
||||||
|
pleasant_perspective=0.0,
|
||||||
|
pleasant_post_processing=0.0,
|
||||||
|
pleasant_reflection=0.0,
|
||||||
|
pleasant_symmetry=0.0,
|
||||||
|
sharply_focused_subject=0.0,
|
||||||
|
tastefully_blurred=0.0,
|
||||||
|
well_chosen_subject=0.0,
|
||||||
|
well_framed_subject=0.0,
|
||||||
|
well_timed_shot=0.0,
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
@@ -21,12 +21,16 @@ import yaml
|
|||||||
from .._constants import (
|
from .._constants import (
|
||||||
_MOVIE_TYPE,
|
_MOVIE_TYPE,
|
||||||
_PHOTO_TYPE,
|
_PHOTO_TYPE,
|
||||||
|
_PHOTOS_4_ALBUM_KIND,
|
||||||
|
_PHOTOS_4_ROOT_FOLDER,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
|
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||||
)
|
)
|
||||||
from ..albuminfo import AlbumInfo
|
from ..albuminfo import AlbumInfo
|
||||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
|
||||||
from ..phototemplate import PhotoTemplate
|
from ..phototemplate import PhotoTemplate
|
||||||
|
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
|
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ class PhotoInfo:
|
|||||||
_xmp_sidecar,
|
_xmp_sidecar,
|
||||||
ExportResults,
|
ExportResults,
|
||||||
)
|
)
|
||||||
|
from ._photoinfo_scoreinfo import score, ScoreInfo
|
||||||
|
|
||||||
def __init__(self, db=None, uuid=None, info=None):
|
def __init__(self, db=None, uuid=None, info=None):
|
||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
@@ -340,21 +345,26 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def albums(self):
|
def albums(self):
|
||||||
""" list of albums picture is contained in """
|
""" list of albums picture is contained in """
|
||||||
albums = []
|
try:
|
||||||
for album in self._info["albums"]:
|
return self._albums
|
||||||
if not self._db._dbalbum_details[album]["intrash"]:
|
except AttributeError:
|
||||||
albums.append(self._db._dbalbum_details[album]["title"])
|
album_uuids = self._get_album_uuids()
|
||||||
return albums
|
self._albums = list(
|
||||||
|
{self._db._dbalbum_details[album]["title"] for album in album_uuids}
|
||||||
|
)
|
||||||
|
return self._albums
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def album_info(self):
|
def album_info(self):
|
||||||
""" list of AlbumInfo objects representing albums the photos is contained in """
|
""" list of AlbumInfo objects representing albums the photos is contained in """
|
||||||
albums = []
|
try:
|
||||||
for album in self._info["albums"]:
|
return self._album_info
|
||||||
if not self._db._dbalbum_details[album]["intrash"]:
|
except AttributeError:
|
||||||
albums.append(AlbumInfo(db=self._db, uuid=album))
|
album_uuids = self._get_album_uuids()
|
||||||
|
self._album_info = [
|
||||||
return albums
|
AlbumInfo(db=self._db, uuid=album) for album in album_uuids
|
||||||
|
]
|
||||||
|
return self._album_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords(self):
|
def keywords(self):
|
||||||
@@ -637,6 +647,9 @@ class PhotoInfo:
|
|||||||
none_str: a str to use if template field renders to None, default is "_".
|
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
|
fields like folder_album; if not provided, defaults to os.path.sep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
"""
|
"""
|
||||||
template = PhotoTemplate(self)
|
template = PhotoTemplate(self)
|
||||||
return template.render(template_str, none_str, path_sep)
|
return template.render(template_str, none_str, path_sep)
|
||||||
@@ -651,6 +664,37 @@ class PhotoInfo:
|
|||||||
""" Returns latitude, in degrees """
|
""" Returns latitude, in degrees """
|
||||||
return self._info["latitude"]
|
return self._info["latitude"]
|
||||||
|
|
||||||
|
def _get_album_uuids(self):
|
||||||
|
""" 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
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
version4 = True
|
||||||
|
album_kind = [_PHOTOS_4_ALBUM_KIND]
|
||||||
|
else:
|
||||||
|
version4 = False
|
||||||
|
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
|
||||||
|
|
||||||
|
album_list = []
|
||||||
|
for album in self._info["albums"]:
|
||||||
|
detail = self._db._dbalbum_details[album]
|
||||||
|
if (
|
||||||
|
detail["kind"] in album_kind
|
||||||
|
and not detail["intrash"]
|
||||||
|
and (
|
||||||
|
not version4
|
||||||
|
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||||
|
# but should not be listed here; they can be distinguished by looking
|
||||||
|
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||||
|
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
album_list.append(album)
|
||||||
|
return album_list
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"osxphotos.{self.__class__.__name__}(db={self._db}, uuid='{self._uuid}', info={self._info})"
|
return f"osxphotos.{self.__class__.__name__}(db={self._db}, uuid='{self._uuid}', info={self._info})"
|
||||||
|
|
||||||
@@ -661,6 +705,8 @@ class PhotoInfo:
|
|||||||
date_modified_iso = (
|
date_modified_iso = (
|
||||||
self.date_modified.isoformat() if self.date_modified else None
|
self.date_modified.isoformat() if self.date_modified else None
|
||||||
)
|
)
|
||||||
|
exif = str(self.exif_info) if self.exif_info else None
|
||||||
|
score = str(self.score) if self.score else None
|
||||||
|
|
||||||
info = {
|
info = {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
@@ -701,6 +747,9 @@ class PhotoInfo:
|
|||||||
"has_raw": self.has_raw,
|
"has_raw": self.has_raw,
|
||||||
"uti_raw": self.uti_raw,
|
"uti_raw": self.uti_raw,
|
||||||
"path_raw": self.path_raw,
|
"path_raw": self.path_raw,
|
||||||
|
"place": self.place,
|
||||||
|
"exif": exif,
|
||||||
|
"score": score,
|
||||||
}
|
}
|
||||||
return yaml.dump(info, sort_keys=False)
|
return yaml.dump(info, sort_keys=False)
|
||||||
|
|
||||||
@@ -713,6 +762,7 @@ class PhotoInfo:
|
|||||||
folders = {album.title: album.folder_names for album in self.album_info}
|
folders = {album.title: album.folder_names for album in self.album_info}
|
||||||
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
||||||
place = self.place.as_dict() if self.place else {}
|
place = self.place.as_dict() if self.place else {}
|
||||||
|
score = dataclasses.asdict(self.score) if self.score else {}
|
||||||
|
|
||||||
pic = {
|
pic = {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
@@ -758,15 +808,22 @@ class PhotoInfo:
|
|||||||
"path_raw": self.path_raw,
|
"path_raw": self.path_raw,
|
||||||
"place": place,
|
"place": place,
|
||||||
"exif": exif,
|
"exif": exif,
|
||||||
|
"score": score,
|
||||||
}
|
}
|
||||||
return json.dumps(pic)
|
return json.dumps(pic)
|
||||||
|
|
||||||
# compare two PhotoInfo objects for equality
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
""" Compare two PhotoInfo objects for equality """
|
||||||
|
# Can't just compare the two __dicts__ because some methods (like albums)
|
||||||
|
# memoize their value once called in an instance variable (e.g. self._albums)
|
||||||
if isinstance(other, self.__class__):
|
if isinstance(other, self.__class__):
|
||||||
return self.__dict__ == other.__dict__
|
return (
|
||||||
|
self._db.db_path == other._db.db_path
|
||||||
|
and self.uuid == other.uuid
|
||||||
|
and self._info == other._info
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
|
""" Compare two PhotoInfo objects for inequality """
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|||||||
145
osxphotos/photosdb/_photosdb_process_scoreinfo.py
Normal file
145
osxphotos/photosdb/_photosdb_process_scoreinfo.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
""" Methods for PhotosDB to add Photos 5 photo score info
|
||||||
|
ref: https://simonwillison.net/2020/May/21/dogsheep-photos/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .._constants import _PHOTOS_4_VERSION
|
||||||
|
from ..utils import _open_sql_file
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module should be imported in the class defintion of PhotosDB in photosdb.py
|
||||||
|
Do not import this module directly
|
||||||
|
This module adds the following method to PhotosDB:
|
||||||
|
_process_scoreinfo: process photo score info
|
||||||
|
|
||||||
|
The following data structures are added to PhotosDB
|
||||||
|
self._db_scoreinfo_uuid
|
||||||
|
|
||||||
|
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _process_scoreinfo(self):
|
||||||
|
""" Process computed photo scores
|
||||||
|
Note: Only works on Photos version == 5.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
# _db_scoreinfo_uuid is dict in form {uuid: {score values}}
|
||||||
|
self._db_scoreinfo_uuid = {}
|
||||||
|
|
||||||
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"search info not implemented for this database version"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_process_scoreinfo_5(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_scoreinfo_5(photosdb):
|
||||||
|
""" Process computed photo scores for Photos 5 databases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photosdb: an OSXPhotosDB instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
db = photosdb._tmp_db
|
||||||
|
|
||||||
|
(conn, cursor) = _open_sql_file(db)
|
||||||
|
|
||||||
|
result = cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
ZGENERICASSET.ZUUID,
|
||||||
|
ZGENERICASSET.ZOVERALLAESTHETICSCORE,
|
||||||
|
ZGENERICASSET.ZCURATIONSCORE,
|
||||||
|
ZGENERICASSET.ZPROMOTIONSCORE,
|
||||||
|
ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
|
||||||
|
ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
|
||||||
|
FROM ZGENERICASSET
|
||||||
|
JOIN ZCOMPUTEDASSETATTRIBUTES ON ZCOMPUTEDASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 0 ZGENERICASSET.ZUUID,
|
||||||
|
# 1 ZGENERICASSET.ZOVERALLAESTHETICSCORE,
|
||||||
|
# 2 ZGENERICASSET.ZCURATIONSCORE,
|
||||||
|
# 3 ZGENERICASSET.ZPROMOTIONSCORE,
|
||||||
|
# 4 ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
|
||||||
|
# 5 ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
|
||||||
|
# 6 ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
|
||||||
|
# 7 ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
|
||||||
|
# 8 ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
|
||||||
|
# 9 ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
|
||||||
|
# 10 ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
|
||||||
|
# 11 ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
|
||||||
|
# 12 ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
|
||||||
|
# 13 ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
|
||||||
|
# 14 ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
|
||||||
|
# 15 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
|
||||||
|
# 16 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
|
||||||
|
# 17 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
|
||||||
|
# 18 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
|
||||||
|
# 19 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
|
||||||
|
# 20 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
|
||||||
|
# 21 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
|
||||||
|
# 22 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
|
||||||
|
# 23 ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
|
||||||
|
# 24 ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
|
||||||
|
# 25 ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
|
||||||
|
# 26 ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
|
||||||
|
# 27 ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
uuid = row[0]
|
||||||
|
scores = {"uuid": uuid}
|
||||||
|
scores["overall_aesthetic"] = row[1]
|
||||||
|
scores["curation"] = row[2]
|
||||||
|
scores["promotion"] = row[3]
|
||||||
|
scores["highlight_visibility"] = row[4]
|
||||||
|
scores["behavioral"] = row[5]
|
||||||
|
scores["failure"] = row[6]
|
||||||
|
scores["harmonious_color"] = row[7]
|
||||||
|
scores["immersiveness"] = row[8]
|
||||||
|
scores["interaction"] = row[9]
|
||||||
|
scores["interesting_subject"] = row[10]
|
||||||
|
scores["intrusive_object_presence"] = row[11]
|
||||||
|
scores["lively_color"] = row[12]
|
||||||
|
scores["low_light"] = row[13]
|
||||||
|
scores["noise"] = row[14]
|
||||||
|
scores["pleasant_camera_tilt"] = row[15]
|
||||||
|
scores["pleasant_composition"] = row[16]
|
||||||
|
scores["pleasant_lighting"] = row[17]
|
||||||
|
scores["pleasant_pattern"] = row[18]
|
||||||
|
scores["pleasant_perspective"] = row[19]
|
||||||
|
scores["pleasant_post_processing"] = row[20]
|
||||||
|
scores["pleasant_reflection"] = row[21]
|
||||||
|
scores["pleasant_symmetry"] = row[22]
|
||||||
|
scores["sharply_focused_subject"] = row[23]
|
||||||
|
scores["tastefully_blurred"] = row[24]
|
||||||
|
scores["well_chosen_subject"] = row[25]
|
||||||
|
scores["well_framed_subject"] = row[26]
|
||||||
|
scores["well_timed_shot"] = row[27]
|
||||||
|
photosdb._db_scoreinfo_uuid[uuid] = scores
|
||||||
@@ -102,7 +102,7 @@ def _process_searchinfo(self):
|
|||||||
# 8: groups.lookup_identifier
|
# 8: groups.lookup_identifier
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = ints_to_uuid(row[1],row[2])
|
uuid = ints_to_uuid(row[1], row[2])
|
||||||
# strings have null character appended, so strip it
|
# strings have null character appended, so strip it
|
||||||
record = {}
|
record = {}
|
||||||
record["uuid"] = uuid
|
record["uuid"] = uuid
|
||||||
@@ -123,13 +123,9 @@ def _process_searchinfo(self):
|
|||||||
|
|
||||||
category = record["category"]
|
category = record["category"]
|
||||||
try:
|
try:
|
||||||
_db_searchinfo_categories[category].append(
|
_db_searchinfo_categories[category].append(record["normalized_string"])
|
||||||
record["normalized_string"]
|
|
||||||
)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_db_searchinfo_categories[category] = [
|
_db_searchinfo_categories[category] = [record["normalized_string"]]
|
||||||
record["normalized_string"]
|
|
||||||
]
|
|
||||||
|
|
||||||
if category == SEARCH_CATEGORY_LABEL:
|
if category == SEARCH_CATEGORY_LABEL:
|
||||||
label = record["content_string"]
|
label = record["content_string"]
|
||||||
@@ -198,6 +194,7 @@ def labels_normalized_as_dict(self):
|
|||||||
|
|
||||||
# The following method is not imported into PhotosDB
|
# The following method is not imported into PhotosDB
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
@lru_cache(maxsize=128)
|
||||||
def ints_to_uuid(uuid_0, uuid_1):
|
def ints_to_uuid(uuid_0, uuid_1):
|
||||||
""" convert two signed ints into a UUID strings
|
""" convert two signed ints into a UUID strings
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||||
# TODO: Fix command line so multiple --keyword, etc. are AND (instead of OR as they are in .photos())
|
|
||||||
# Or fix the help text to match behavior
|
|
||||||
# TODO: Add test for __str__
|
# TODO: Add test for __str__
|
||||||
# TODO: Add special albums and magic albums
|
# TODO: Add special albums and magic albums
|
||||||
|
|
||||||
@@ -64,6 +62,7 @@ class PhotosDB:
|
|||||||
labels_as_dict,
|
labels_as_dict,
|
||||||
labels_normalized_as_dict,
|
labels_normalized_as_dict,
|
||||||
)
|
)
|
||||||
|
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
||||||
|
|
||||||
def __init__(self, *dbfile_, dbfile=None):
|
def __init__(self, *dbfile_, dbfile=None):
|
||||||
""" create a new PhotosDB object
|
""" create a new PhotosDB object
|
||||||
@@ -254,7 +253,7 @@ class PhotosDB:
|
|||||||
if _db_is_locked(self._dbfile):
|
if _db_is_locked(self._dbfile):
|
||||||
self._tmp_db = self._copy_db_file(self._dbfile)
|
self._tmp_db = self._copy_db_file(self._dbfile)
|
||||||
|
|
||||||
self._db_version = self._get_db_version()
|
self._db_version = self._get_db_version(self._tmp_db)
|
||||||
|
|
||||||
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
|
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
|
||||||
if int(self._db_version) > int(_PHOTOS_4_VERSION):
|
if int(self._db_version) > int(_PHOTOS_4_VERSION):
|
||||||
@@ -294,18 +293,17 @@ class PhotosDB:
|
|||||||
@property
|
@property
|
||||||
def keywords_as_dict(self):
|
def keywords_as_dict(self):
|
||||||
""" return keywords as dict of keyword, count in reverse sorted order (descending) """
|
""" return keywords as dict of keyword, count in reverse sorted order (descending) """
|
||||||
keywords = {}
|
keywords = {
|
||||||
for k in self._dbkeywords_keyword.keys():
|
k: len(self._dbkeywords_keyword[k]) for k in self._dbkeywords_keyword.keys()
|
||||||
keywords[k] = len(self._dbkeywords_keyword[k])
|
}
|
||||||
|
|
||||||
keywords = dict(sorted(keywords.items(), key=lambda kv: kv[1], reverse=True))
|
keywords = dict(sorted(keywords.items(), key=lambda kv: kv[1], reverse=True))
|
||||||
return keywords
|
return keywords
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def persons_as_dict(self):
|
def persons_as_dict(self):
|
||||||
""" return persons as dict of person, count in reverse sorted order (descending) """
|
""" return persons as dict of person, count in reverse sorted order (descending) """
|
||||||
persons = {}
|
persons = {k: len(self._dbfaces_person[k]) for k in self._dbfaces_person.keys()}
|
||||||
for k in self._dbfaces_person.keys():
|
|
||||||
persons[k] = len(self._dbfaces_person[k])
|
|
||||||
persons = dict(sorted(persons.items(), key=lambda kv: kv[1], reverse=True))
|
persons = dict(sorted(persons.items(), key=lambda kv: kv[1], reverse=True))
|
||||||
return persons
|
return persons
|
||||||
|
|
||||||
@@ -313,18 +311,16 @@ class PhotosDB:
|
|||||||
def albums_as_dict(self):
|
def albums_as_dict(self):
|
||||||
""" return albums as dict of albums, count in reverse sorted order (descending) """
|
""" return albums as dict of albums, count in reverse sorted order (descending) """
|
||||||
albums = {}
|
albums = {}
|
||||||
album_keys = [
|
album_keys = self._get_album_uuids(shared=False)
|
||||||
k
|
for album in album_keys:
|
||||||
for k in self._dbalbums_album.keys()
|
title = self._dbalbum_details[album]["title"]
|
||||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
|
if album in self._dbalbums_album:
|
||||||
and not self._dbalbum_details[k]["intrash"]
|
try:
|
||||||
]
|
albums[title] += len(self._dbalbums_album[album])
|
||||||
for k in album_keys:
|
except KeyError:
|
||||||
title = self._dbalbum_details[k]["title"]
|
albums[title] = len(self._dbalbums_album[album])
|
||||||
if title in albums:
|
|
||||||
albums[title] += len(self._dbalbums_album[k])
|
|
||||||
else:
|
else:
|
||||||
albums[title] = len(self._dbalbums_album[k])
|
albums[title] = 0 # empty album
|
||||||
albums = dict(sorted(albums.items(), key=lambda kv: kv[1], reverse=True))
|
albums = dict(sorted(albums.items(), key=lambda kv: kv[1], reverse=True))
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
@@ -333,25 +329,17 @@ class PhotosDB:
|
|||||||
""" returns shared albums as dict of albums, count in reverse sorted order (descending)
|
""" returns shared albums as dict of albums, count in reverse sorted order (descending)
|
||||||
valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict """
|
valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict """
|
||||||
|
|
||||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
logging.warning(
|
|
||||||
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
albums = {}
|
albums = {}
|
||||||
album_keys = [
|
album_keys = self._get_album_uuids(shared=True)
|
||||||
k
|
for album in album_keys:
|
||||||
for k in self._dbalbums_album.keys()
|
title = self._dbalbum_details[album]["title"]
|
||||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is not None
|
if album in self._dbalbums_album:
|
||||||
]
|
try:
|
||||||
for k in album_keys:
|
albums[title] += len(self._dbalbums_album[album])
|
||||||
title = self._dbalbum_details[k]["title"]
|
except KeyError:
|
||||||
if title in albums:
|
albums[title] = len(self._dbalbums_album[album])
|
||||||
albums[title] += len(self._dbalbums_album[k])
|
|
||||||
else:
|
else:
|
||||||
albums[title] = len(self._dbalbums_album[k])
|
albums[title] = 0 # empty album
|
||||||
albums = dict(sorted(albums.items(), key=lambda kv: kv[1], reverse=True))
|
albums = dict(sorted(albums.items(), key=lambda kv: kv[1], reverse=True))
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
@@ -412,34 +400,28 @@ class PhotosDB:
|
|||||||
@property
|
@property
|
||||||
def album_info(self):
|
def album_info(self):
|
||||||
""" return list of AlbumInfo objects for each album in the photos database """
|
""" return list of AlbumInfo objects for each album in the photos database """
|
||||||
|
try:
|
||||||
albums = [
|
return self._album_info
|
||||||
AlbumInfo(db=self, uuid=album)
|
except AttributeError:
|
||||||
for album in self._dbalbums_album.keys()
|
self._album_info = [
|
||||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
AlbumInfo(db=self, uuid=album)
|
||||||
and not self._dbalbum_details[album]["intrash"]
|
for album in self._get_album_uuids(shared=False)
|
||||||
]
|
]
|
||||||
return albums
|
return self._album_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def album_info_shared(self):
|
def album_info_shared(self):
|
||||||
""" return list of AlbumInfo objects for each shared album in the photos database
|
""" return list of AlbumInfo objects for each shared album in the photos database
|
||||||
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
|
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
|
||||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
||||||
|
try:
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
return self._album_info_shared
|
||||||
logging.warning(
|
except AttributeError:
|
||||||
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
self._album_info_shared = [
|
||||||
)
|
AlbumInfo(db=self, uuid=album)
|
||||||
return []
|
for album in self._get_album_uuids(shared=True)
|
||||||
|
]
|
||||||
albums_shared = [
|
return self._album_info_shared
|
||||||
AlbumInfo(db=self, uuid=album)
|
|
||||||
for album in self._dbalbums_album.keys()
|
|
||||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
|
|
||||||
and not self._dbalbum_details[album]["intrash"]
|
|
||||||
]
|
|
||||||
return albums_shared
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def albums(self):
|
def albums(self):
|
||||||
@@ -448,13 +430,11 @@ class PhotosDB:
|
|||||||
# Could be more than one album with same name
|
# Could be more than one album with same name
|
||||||
# Right now, they are treated as same album and photos are combined from albums with same name
|
# Right now, they are treated as same album and photos are combined from albums with same name
|
||||||
|
|
||||||
albums = {
|
try:
|
||||||
self._dbalbum_details[album]["title"]
|
return self._albums
|
||||||
for album in self._dbalbums_album.keys()
|
except AttributeError:
|
||||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
self._albums = self._get_albums(shared=False)
|
||||||
and not self._dbalbum_details[album]["intrash"]
|
return self._albums
|
||||||
}
|
|
||||||
return list(albums)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def albums_shared(self):
|
def albums_shared(self):
|
||||||
@@ -466,19 +446,11 @@ class PhotosDB:
|
|||||||
|
|
||||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
||||||
|
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
try:
|
||||||
logging.warning(
|
return self._albums_shared
|
||||||
f"album_names_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
except AttributeError:
|
||||||
)
|
self._albums_shared = self._get_albums(shared=True)
|
||||||
return []
|
return self._albums_shared
|
||||||
|
|
||||||
albums = {
|
|
||||||
self._dbalbum_details[album]["title"]
|
|
||||||
for album in self._dbalbums_album.keys()
|
|
||||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
|
|
||||||
and not self._dbalbum_details[album]["intrash"]
|
|
||||||
}
|
|
||||||
return list(albums)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db_version(self):
|
def db_version(self):
|
||||||
@@ -495,6 +467,14 @@ class PhotosDB:
|
|||||||
""" returns path to the Photos library PhotosDB was initialized with """
|
""" returns path to the Photos library PhotosDB was initialized with """
|
||||||
return self._library_path
|
return self._library_path
|
||||||
|
|
||||||
|
def get_db_connection(self):
|
||||||
|
""" Get connection to the working copy of the Photos database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple of (connection, cursor) to sqlite3 database
|
||||||
|
"""
|
||||||
|
return _open_sql_file(self._tmp_db)
|
||||||
|
|
||||||
def _copy_db_file(self, fname):
|
def _copy_db_file(self, fname):
|
||||||
""" copies the sqlite database file to a temp file """
|
""" copies the sqlite database file to a temp file """
|
||||||
""" returns the name of the temp file """
|
""" returns the name of the temp file """
|
||||||
@@ -519,12 +499,18 @@ class PhotosDB:
|
|||||||
|
|
||||||
return dest_path
|
return dest_path
|
||||||
|
|
||||||
def _get_db_version(self):
|
def _get_db_version(self, db_file):
|
||||||
""" gets the Photos DB version from LiGlobals table """
|
""" Gets the Photos DB version from LiGlobals table
|
||||||
""" returns the version as str"""
|
|
||||||
|
Args:
|
||||||
|
db_file: path to database file containing LiGlobals table
|
||||||
|
|
||||||
|
Returns: version as str
|
||||||
|
"""
|
||||||
|
|
||||||
version = None
|
version = None
|
||||||
|
|
||||||
(conn, c) = _open_sql_file(self._tmp_db)
|
(conn, c) = _open_sql_file(db_file)
|
||||||
|
|
||||||
# get database version
|
# get database version
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -624,6 +610,8 @@ class PhotosDB:
|
|||||||
"folderUuid": album[5],
|
"folderUuid": album[5],
|
||||||
"albumType": album[6],
|
"albumType": album[6],
|
||||||
"albumSubclass": album[7],
|
"albumSubclass": album[7],
|
||||||
|
# for compatability with Photos 5 where album kind is ZKIND
|
||||||
|
"kind": album[7],
|
||||||
}
|
}
|
||||||
|
|
||||||
# get details about folders
|
# get details about folders
|
||||||
@@ -1724,8 +1712,6 @@ class PhotosDB:
|
|||||||
# determine if a photo is missing in Photos 5
|
# determine if a photo is missing in Photos 5
|
||||||
|
|
||||||
# Get info on remote/local availability for photos in shared albums
|
# Get info on remote/local availability for photos in shared albums
|
||||||
# Shared photos have a null fingerprint (and some other photos do too)
|
|
||||||
# TODO: There may be a bug here, perhaps ZDATASTORESUBTYPE should be 1 --> it's the longest ZDATALENGTH (is this the original)
|
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
ZGENERICASSET.ZUUID,
|
ZGENERICASSET.ZUUID,
|
||||||
@@ -1734,10 +1720,7 @@ class PhotosDB:
|
|||||||
FROM ZGENERICASSET
|
FROM ZGENERICASSET
|
||||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||||
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
|
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
|
||||||
WHERE ZDATASTORESUBTYPE = 0 OR ZDATASTORESUBTYPE = 3 """
|
WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
|
||||||
# WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
|
|
||||||
# WHERE ZDATASTORESUBTYPE = 0 OR ZDATASTORESUBTYPE = 3 """
|
|
||||||
# WHERE ZINTERNALRESOURCE.ZFINGERPRINT IS NULL AND ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 3 """
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
@@ -1870,6 +1853,9 @@ class PhotosDB:
|
|||||||
# process exif info
|
# process exif info
|
||||||
self._process_exifinfo()
|
self._process_exifinfo()
|
||||||
|
|
||||||
|
# process computed scores
|
||||||
|
self._process_scoreinfo()
|
||||||
|
|
||||||
# done processing, dump debug data if requested
|
# done processing, dump debug data if requested
|
||||||
if _debug():
|
if _debug():
|
||||||
logging.debug("Faces (_dbfaces_uuid):")
|
logging.debug("Faces (_dbfaces_uuid):")
|
||||||
@@ -2090,6 +2076,65 @@ class PhotosDB:
|
|||||||
hierarchy = _recurse_folder_hierarchy(folders)
|
hierarchy = _recurse_folder_hierarchy(folders)
|
||||||
return hierarchy
|
return hierarchy
|
||||||
|
|
||||||
|
def _get_album_uuids(self, shared=False):
|
||||||
|
""" Return list of album UUIDs found in photos database
|
||||||
|
|
||||||
|
Filters out albums in the trash and any special album types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shared: boolean; if True, returns shared albums, else normal albums
|
||||||
|
|
||||||
|
Returns: list of album UUIDs
|
||||||
|
"""
|
||||||
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
version4 = True
|
||||||
|
if shared:
|
||||||
|
logging.warning(
|
||||||
|
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||||
|
)
|
||||||
|
return [] # not implemented for _PHOTOS_4_VERSION
|
||||||
|
else:
|
||||||
|
album_kind = _PHOTOS_4_ALBUM_KIND
|
||||||
|
else:
|
||||||
|
version4 = False
|
||||||
|
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND if shared else _PHOTOS_5_ALBUM_KIND
|
||||||
|
|
||||||
|
album_list = []
|
||||||
|
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||||
|
for album, detail in self._dbalbum_details.items():
|
||||||
|
if (
|
||||||
|
detail["kind"] == album_kind
|
||||||
|
and not detail["intrash"]
|
||||||
|
and (
|
||||||
|
(shared and detail["cloudownerhashedpersonid"] is not None)
|
||||||
|
or (not shared and detail["cloudownerhashedpersonid"] is None)
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
not version4
|
||||||
|
# in Photos 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
|
||||||
|
# but should not be listed here; they can be distinguished by looking
|
||||||
|
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||||
|
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
album_list.append(album)
|
||||||
|
return album_list
|
||||||
|
|
||||||
|
def _get_albums(self, shared=False):
|
||||||
|
""" Return list of album titles found in photos database
|
||||||
|
Albums may have duplicate titles -- these will be treated as a single album.
|
||||||
|
|
||||||
|
Filters out albums in the trash and any special album types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shared: boolean; if True, returns shared albums, else normal albums
|
||||||
|
|
||||||
|
Returns: list of album names
|
||||||
|
"""
|
||||||
|
|
||||||
|
album_uuids = self._get_album_uuids(shared=shared)
|
||||||
|
return list({self._dbalbum_details[album]["title"] for album in album_uuids})
|
||||||
|
|
||||||
def photos(
|
def photos(
|
||||||
self,
|
self,
|
||||||
keywords=None,
|
keywords=None,
|
||||||
@@ -2136,7 +2181,11 @@ class PhotosDB:
|
|||||||
if album in self._dbalbum_titles:
|
if album in self._dbalbum_titles:
|
||||||
title_set = set()
|
title_set = set()
|
||||||
for album_id in self._dbalbum_titles[album]:
|
for album_id in self._dbalbum_titles[album]:
|
||||||
title_set.update(self._dbalbums_album[album_id])
|
try:
|
||||||
|
title_set.update(self._dbalbums_album[album_id])
|
||||||
|
except KeyError:
|
||||||
|
# an empty album will be in _dbalbum_titles but not _dbalbums_album
|
||||||
|
pass
|
||||||
album_set.update(title_set)
|
album_set.update(title_set)
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Could not find album '{album}' in database")
|
logging.debug(f"Could not find album '{album}' in database")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
""" Custom template system for osxphotos (implemented in PhotoInfo.render_template) """
|
""" Custom template system for osxphotos (implemented in PhotoInfo.render_template) """
|
||||||
|
|
||||||
|
|
||||||
# Rolled my own template system because:
|
# Rolled my own template system because:
|
||||||
# 1. Needed to handle multiple values (e.g. album, keyword)
|
# 1. Needed to handle multiple values (e.g. album, keyword)
|
||||||
# 2. Needed to handle default values if template not found
|
# 2. Needed to handle default values if template not found
|
||||||
@@ -8,7 +9,6 @@
|
|||||||
# 4. Couldn't figure out how to do #1 and #2 with str.format()
|
# 4. Couldn't figure out how to do #1 and #2 with str.format()
|
||||||
#
|
#
|
||||||
# This code isn't elegant but it seems to work well. PRs gladly accepted.
|
# This code isn't elegant but it seems to work well. PRs gladly accepted.
|
||||||
|
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -35,6 +35,14 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{created.dd}": "2-digit day of the month (zero padded) of file creation time",
|
"{created.dd}": "2-digit day of the month (zero padded) of file creation time",
|
||||||
"{created.dow}": "Day of week in user's locale of the file creation time",
|
"{created.dow}": "Day of week in user's locale of the file creation time",
|
||||||
"{created.doy}": "3-digit day of year (e.g Julian day) of file creation time, starting from 1 (zero padded)",
|
"{created.doy}": "3-digit day of year (e.g Julian day) of file creation time, starting from 1 (zero padded)",
|
||||||
|
"{created.hour}": "2-digit hour of the file creation time",
|
||||||
|
"{created.min}": "2-digit minute of the file creation time",
|
||||||
|
"{created.sec}": "2-digit second of the file creation time",
|
||||||
|
"{created.strftime}": "Apply strftime template to file creation date/time. Should be used in form "
|
||||||
|
+ "{created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
|
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||||
|
+ "If used with no template will return null value. "
|
||||||
|
+ "See https://strftime.org/ for help on strftime templates.",
|
||||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||||
"{modified.year}": "4-digit year of file modification time",
|
"{modified.year}": "4-digit year of file modification time",
|
||||||
"{modified.yy}": "2-digit year of file modification time",
|
"{modified.yy}": "2-digit year of file modification time",
|
||||||
@@ -43,6 +51,14 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{modified.mon}": "Month abbreviation in the user's locale of the file modification time",
|
"{modified.mon}": "Month abbreviation in the user's locale of the file modification time",
|
||||||
"{modified.dd}": "2-digit day of the month (zero padded) of the file modification time",
|
"{modified.dd}": "2-digit day of the month (zero padded) of the file modification time",
|
||||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)",
|
"{modified.doy}": "3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)",
|
||||||
|
"{modified.hour}": "2-digit hour of the file modification time",
|
||||||
|
"{modified.min}": "2-digit minute of the file modification time",
|
||||||
|
"{modified.sec}": "2-digit second of the file modification time",
|
||||||
|
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||||
|
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
|
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||||
|
# + "If used with no template will return null value. "
|
||||||
|
# + "See https://strftime.org/ for help on strftime templates.",
|
||||||
"{place.name}": "Place name from the photo's reverse geolocation data, as displayed in Photos",
|
"{place.name}": "Place name from the photo's reverse geolocation data, as displayed in Photos",
|
||||||
"{place.country_code}": "The ISO country code from the photo's reverse geolocation data",
|
"{place.country_code}": "The ISO country code from the photo's reverse geolocation data",
|
||||||
"{place.name.country}": "Country name from the photo's reverse geolocation data",
|
"{place.name.country}": "Country name from the photo's reverse geolocation data",
|
||||||
@@ -71,7 +87,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
# Just the multi-valued substitution names without the braces
|
# Just the multi-valued substitution names without the braces
|
||||||
MULTI_VALUE_SUBSTITUTIONS = [
|
MULTI_VALUE_SUBSTITUTIONS = [
|
||||||
field.replace("{", "").replace("}", "")
|
field.replace("{", "").replace("}", "")
|
||||||
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.keys()
|
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -87,10 +103,17 @@ class PhotoTemplate:
|
|||||||
self.photo = photo
|
self.photo = photo
|
||||||
|
|
||||||
def render(self, template, none_str="_", path_sep=None):
|
def render(self, template, none_str="_", path_sep=None):
|
||||||
""" render a filename or directory template
|
""" Render a filename or directory template
|
||||||
|
|
||||||
|
Args:
|
||||||
template: str template
|
template: str template
|
||||||
none_str: str to use default for None values, default is '_'
|
none_str: str to use default for None values, default is '_'
|
||||||
path_sep: optional character to use as path separator, default is os.path.sep """
|
path_sep: optional character to use as path separator, default is os.path.sep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
|
"""
|
||||||
|
|
||||||
if path_sep is None:
|
if path_sep is None:
|
||||||
path_sep = os.path.sep
|
path_sep = os.path.sep
|
||||||
elif path_sep is not None and len(path_sep) != 1:
|
elif path_sep is not None and len(path_sep) != 1:
|
||||||
@@ -107,7 +130,7 @@ class PhotoTemplate:
|
|||||||
# regex to find {template_field,optional_default} in strings
|
# regex to find {template_field,optional_default} in strings
|
||||||
# for explanation of regex see https://regex101.com/r/4JJg42/1
|
# for explanation of regex see https://regex101.com/r/4JJg42/1
|
||||||
# pylint: disable=anomalous-backslash-in-string
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-. ]+))?)(?=\}(?!\}))\}"
|
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-\%. ]+))?)(?=\}(?!\}))\}"
|
||||||
if type(template) is not str:
|
if type(template) is not str:
|
||||||
raise TypeError(f"template must be type str, not {type(template)}")
|
raise TypeError(f"template must be type str, not {type(template)}")
|
||||||
|
|
||||||
@@ -122,7 +145,7 @@ class PhotoTemplate:
|
|||||||
groups = len(matchobj.groups())
|
groups = len(matchobj.groups())
|
||||||
if groups == 4:
|
if groups == 4:
|
||||||
try:
|
try:
|
||||||
val = get_func(matchobj.group(1))
|
val = get_func(matchobj.group(1), matchobj.group(3))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return matchobj.group(0)
|
return matchobj.group(0)
|
||||||
|
|
||||||
@@ -172,7 +195,7 @@ class PhotoTemplate:
|
|||||||
rendered_strings = set([rendered])
|
rendered_strings = set([rendered])
|
||||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||||
# Build a regex that matches only the field being processed
|
# Build a regex that matches only the field being processed
|
||||||
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-. ]{0,})))?\}"
|
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-\%. ]{0,})))?\}"
|
||||||
regex_multi = re.compile(re_str)
|
regex_multi = re.compile(re_str)
|
||||||
|
|
||||||
# holds each of the new rendered_strings, set() to avoid duplicates
|
# holds each of the new rendered_strings, set() to avoid duplicates
|
||||||
@@ -183,10 +206,11 @@ class PhotoTemplate:
|
|||||||
values = self.get_template_value_multi(field, path_sep)
|
values = self.get_template_value_multi(field, path_sep)
|
||||||
for val in values:
|
for val in values:
|
||||||
|
|
||||||
def lookup_template_value_multi(lookup_value):
|
def lookup_template_value_multi(lookup_value, default):
|
||||||
""" Closure passed to make_subst_function get_func
|
""" Closure passed to make_subst_function get_func
|
||||||
Capture val and field in the closure
|
Capture val and field in the closure
|
||||||
Allows make_subst_function to be re-used w/o modification """
|
Allows make_subst_function to be re-used w/o modification
|
||||||
|
default is not used but required so signature matches get_template_value """
|
||||||
if lookup_value == field:
|
if lookup_value == field:
|
||||||
return val
|
return val
|
||||||
else:
|
else:
|
||||||
@@ -220,11 +244,12 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
return rendered_strings, unmatched
|
return rendered_strings, unmatched
|
||||||
|
|
||||||
def get_template_value(self, field):
|
def get_template_value(self, field, default):
|
||||||
"""lookup value for template field (single-value template substitutions)
|
"""lookup value for template field (single-value template substitutions)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field: template field to find value for.
|
field: template field to find value for.
|
||||||
|
default: the default value provided by the user
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The matching template value (which may be None).
|
The matching template value (which may be None).
|
||||||
@@ -234,172 +259,228 @@ class PhotoTemplate:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# must be a valid keyword
|
# must be a valid keyword
|
||||||
if field =="name":
|
if field == "name":
|
||||||
return pathlib.Path(self.photo.filename).stem
|
return pathlib.Path(self.photo.filename).stem
|
||||||
|
|
||||||
if field =="original_name":
|
if field == "original_name":
|
||||||
return pathlib.Path(self.photo.original_filename).stem
|
return pathlib.Path(self.photo.original_filename).stem
|
||||||
|
|
||||||
if field =="title":
|
if field == "title":
|
||||||
return self.photo.title
|
return self.photo.title
|
||||||
|
|
||||||
if field =="descr":
|
if field == "descr":
|
||||||
return self.photo.description
|
return self.photo.description
|
||||||
|
|
||||||
if field =="created.date":
|
if field == "created.date":
|
||||||
return DateTimeFormatter(self.photo.date).date
|
return DateTimeFormatter(self.photo.date).date
|
||||||
|
|
||||||
if field =="created.year":
|
if field == "created.year":
|
||||||
return DateTimeFormatter(self.photo.date).year
|
return DateTimeFormatter(self.photo.date).year
|
||||||
|
|
||||||
if field =="created.yy":
|
if field == "created.yy":
|
||||||
return DateTimeFormatter(self.photo.date).yy
|
return DateTimeFormatter(self.photo.date).yy
|
||||||
|
|
||||||
if field =="created.mm":
|
if field == "created.mm":
|
||||||
return DateTimeFormatter(self.photo.date).mm
|
return DateTimeFormatter(self.photo.date).mm
|
||||||
|
|
||||||
if field =="created.month":
|
if field == "created.month":
|
||||||
return DateTimeFormatter(self.photo.date).month
|
return DateTimeFormatter(self.photo.date).month
|
||||||
|
|
||||||
if field =="created.mon":
|
if field == "created.mon":
|
||||||
return DateTimeFormatter(self.photo.date).mon
|
return DateTimeFormatter(self.photo.date).mon
|
||||||
|
|
||||||
if field =="created.dd":
|
if field == "created.dd":
|
||||||
return DateTimeFormatter(self.photo.date).dd
|
return DateTimeFormatter(self.photo.date).dd
|
||||||
|
|
||||||
if field =="created.dow":
|
if field == "created.dow":
|
||||||
return DateTimeFormatter(self.photo.date).dow
|
return DateTimeFormatter(self.photo.date).dow
|
||||||
|
|
||||||
if field =="created.doy":
|
if field == "created.doy":
|
||||||
return DateTimeFormatter(self.photo.date).doy
|
return DateTimeFormatter(self.photo.date).doy
|
||||||
|
|
||||||
if field =="modified.date":
|
if field == "created.hour":
|
||||||
|
return DateTimeFormatter(self.photo.date).hour
|
||||||
|
|
||||||
|
if field == "created.min":
|
||||||
|
return DateTimeFormatter(self.photo.date).min
|
||||||
|
|
||||||
|
if field == "created.sec":
|
||||||
|
return DateTimeFormatter(self.photo.date).sec
|
||||||
|
|
||||||
|
if field == "created.strftime":
|
||||||
|
if default:
|
||||||
|
try:
|
||||||
|
return self.photo.date.strftime(default)
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if field == "modified.date":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).date
|
DateTimeFormatter(self.photo.date_modified).date
|
||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.year":
|
if field == "modified.year":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).year
|
DateTimeFormatter(self.photo.date_modified).year
|
||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.yy":
|
if field == "modified.yy":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).yy if self.photo.date_modified else None
|
DateTimeFormatter(self.photo.date_modified).yy
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.mm":
|
if field == "modified.mm":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).mm if self.photo.date_modified else None
|
DateTimeFormatter(self.photo.date_modified).mm
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.month":
|
if field == "modified.month":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).month
|
DateTimeFormatter(self.photo.date_modified).month
|
||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.mon":
|
if field == "modified.mon":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).mon
|
DateTimeFormatter(self.photo.date_modified).mon
|
||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.dd":
|
if field == "modified.dd":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).dd if self.photo.date_modified else None
|
DateTimeFormatter(self.photo.date_modified).dd
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="modified.doy":
|
if field == "modified.doy":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).doy
|
DateTimeFormatter(self.photo.date_modified).doy
|
||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.name":
|
if field == "modified.hour":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(self.photo.date_modified).hour
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if field == "modified.min":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(self.photo.date_modified).min
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if field == "modified.sec":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(self.photo.date_modified).sec
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: disabling modified.strftime for now because now clean way to pass
|
||||||
|
# a default value if modified time is None
|
||||||
|
# if field == "modified.strftime":
|
||||||
|
# if default and self.photo.date_modified:
|
||||||
|
# try:
|
||||||
|
# return self.photo.date_modified.strftime(default)
|
||||||
|
# except:
|
||||||
|
# raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
|
# else:
|
||||||
|
# return None
|
||||||
|
|
||||||
|
if field == "place.name":
|
||||||
return self.photo.place.name if self.photo.place else None
|
return self.photo.place.name if self.photo.place else None
|
||||||
|
|
||||||
if field =="place.country_code":
|
if field == "place.country_code":
|
||||||
return self.photo.place.country_code if self.photo.place else None
|
return self.photo.place.country_code if self.photo.place else None
|
||||||
|
|
||||||
if field =="place.name.country":
|
if field == "place.name.country":
|
||||||
return (
|
return (
|
||||||
self.photo.place.names.country[0]
|
self.photo.place.names.country[0]
|
||||||
if self.photo.place and self.photo.place.names.country
|
if self.photo.place and self.photo.place.names.country
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.name.state_province":
|
if field == "place.name.state_province":
|
||||||
return (
|
return (
|
||||||
self.photo.place.names.state_province[0]
|
self.photo.place.names.state_province[0]
|
||||||
if self.photo.place and self.photo.place.names.state_province
|
if self.photo.place and self.photo.place.names.state_province
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.name.city":
|
if field == "place.name.city":
|
||||||
return (
|
return (
|
||||||
self.photo.place.names.city[0]
|
self.photo.place.names.city[0]
|
||||||
if self.photo.place and self.photo.place.names.city
|
if self.photo.place and self.photo.place.names.city
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.name.area_of_interest":
|
if field == "place.name.area_of_interest":
|
||||||
return (
|
return (
|
||||||
self.photo.place.names.area_of_interest[0]
|
self.photo.place.names.area_of_interest[0]
|
||||||
if self.photo.place and self.photo.place.names.area_of_interest
|
if self.photo.place and self.photo.place.names.area_of_interest
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address":
|
if field == "place.address":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address_str
|
self.photo.place.address_str
|
||||||
if self.photo.place and self.photo.place.address_str
|
if self.photo.place and self.photo.place.address_str
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.street":
|
if field == "place.address.street":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.street
|
self.photo.place.address.street
|
||||||
if self.photo.place and self.photo.place.address.street
|
if self.photo.place and self.photo.place.address.street
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.city":
|
if field == "place.address.city":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.city
|
self.photo.place.address.city
|
||||||
if self.photo.place and self.photo.place.address.city
|
if self.photo.place and self.photo.place.address.city
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.state_province":
|
if field == "place.address.state_province":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.state_province
|
self.photo.place.address.state_province
|
||||||
if self.photo.place and self.photo.place.address.state_province
|
if self.photo.place and self.photo.place.address.state_province
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.postal_code":
|
if field == "place.address.postal_code":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.postal_code
|
self.photo.place.address.postal_code
|
||||||
if self.photo.place and self.photo.place.address.postal_code
|
if self.photo.place and self.photo.place.address.postal_code
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.country":
|
if field == "place.address.country":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.country
|
self.photo.place.address.country
|
||||||
if self.photo.place and self.photo.place.address.country
|
if self.photo.place and self.photo.place.address.country
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if field =="place.address.country_code":
|
if field == "place.address.country_code":
|
||||||
return (
|
return (
|
||||||
self.photo.place.address.iso_country_code
|
self.photo.place.address.iso_country_code
|
||||||
if self.photo.place and self.photo.place.address.iso_country_code
|
if self.photo.place and self.photo.place.address.iso_country_code
|
||||||
|
|||||||
@@ -87,19 +87,19 @@ class PLRevGeoLocationInfo:
|
|||||||
self.postalAddress = postalAddress
|
self.postalAddress = postalAddress
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
for field in [
|
return all(
|
||||||
"addressString",
|
getattr(self, field) == getattr(other, field)
|
||||||
"countryCode",
|
for field in [
|
||||||
"isHome",
|
"addressString",
|
||||||
"compoundNames",
|
"countryCode",
|
||||||
"compoundSecondaryNames",
|
"isHome",
|
||||||
"version",
|
"compoundNames",
|
||||||
"geoServiceProvider",
|
"compoundSecondaryNames",
|
||||||
"postalAddress",
|
"version",
|
||||||
]:
|
"geoServiceProvider",
|
||||||
if getattr(self, field) != getattr(other, field):
|
"postalAddress",
|
||||||
return False
|
]
|
||||||
return True
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@@ -151,21 +151,17 @@ class PLRevGeoMapItem:
|
|||||||
self.finalPlaceInfos = finalPlaceInfos
|
self.finalPlaceInfos = finalPlaceInfos
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
for field in ["sortedPlaceInfos", "finalPlaceInfos"]:
|
return all(
|
||||||
if getattr(self, field) != getattr(other, field):
|
getattr(self, field) == getattr(other, field)
|
||||||
return False
|
for field in ["sortedPlaceInfos", "finalPlaceInfos"]
|
||||||
return True
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
sortedPlaceInfos = []
|
sortedPlaceInfos = [str(place) for place in self.sortedPlaceInfos]
|
||||||
finalPlaceInfos = []
|
finalPlaceInfos = [str(place) for place in self.finalPlaceInfos]
|
||||||
for place in self.sortedPlaceInfos:
|
|
||||||
sortedPlaceInfos.append(str(place))
|
|
||||||
for place in self.finalPlaceInfos:
|
|
||||||
finalPlaceInfos.append(str(place))
|
|
||||||
return (
|
return (
|
||||||
f"finalPlaceInfos: {finalPlaceInfos}, sortedPlaceInfos: {sortedPlaceInfos}"
|
f"finalPlaceInfos: {finalPlaceInfos}, sortedPlaceInfos: {sortedPlaceInfos}"
|
||||||
)
|
)
|
||||||
@@ -192,10 +188,10 @@ class PLRevGeoMapItemAdditionalPlaceInfo:
|
|||||||
self.dominantOrderType = dominantOrderType
|
self.dominantOrderType = dominantOrderType
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
for field in ["area", "name", "placeType", "dominantOrderType"]:
|
return all(
|
||||||
if getattr(self, field) != getattr(other, field):
|
getattr(self, field) == getattr(other, field)
|
||||||
return False
|
for field in ["area", "name", "placeType", "dominantOrderType"]
|
||||||
return True
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@@ -245,19 +241,19 @@ class CNPostalAddress:
|
|||||||
self._subLocality = _subLocality
|
self._subLocality = _subLocality
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
for field in [
|
return all(
|
||||||
"_ISOCountryCode",
|
getattr(self, field) == getattr(other, field)
|
||||||
"_city",
|
for field in [
|
||||||
"_country",
|
"_ISOCountryCode",
|
||||||
"_postalCode",
|
"_city",
|
||||||
"_state",
|
"_country",
|
||||||
"_street",
|
"_postalCode",
|
||||||
"_subAdministrativeArea",
|
"_state",
|
||||||
"_subLocality",
|
"_street",
|
||||||
]:
|
"_subAdministrativeArea",
|
||||||
if getattr(self, field) != getattr(other, field):
|
"_subLocality",
|
||||||
return False
|
]
|
||||||
return True
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@@ -490,16 +486,14 @@ class PlaceInfo4(PlaceInfo):
|
|||||||
"names": self.names,
|
"names": self.names,
|
||||||
"country_code": self.country_code,
|
"country_code": self.country_code,
|
||||||
}
|
}
|
||||||
strval = "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
||||||
return strval
|
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
info = {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"names": self.names._asdict(),
|
"names": self.names._asdict(),
|
||||||
"country_code": self.country_code,
|
"country_code": self.country_code,
|
||||||
}
|
}
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class PlaceInfo5(PlaceInfo):
|
class PlaceInfo5(PlaceInfo):
|
||||||
@@ -509,7 +503,6 @@ class PlaceInfo5(PlaceInfo):
|
|||||||
""" revgeoloc_bplist: a binary plist blob containing
|
""" revgeoloc_bplist: a binary plist blob containing
|
||||||
a serialized PLRevGeoLocationInfo object """
|
a serialized PLRevGeoLocationInfo object """
|
||||||
self._bplist = revgeoloc_bplist
|
self._bplist = revgeoloc_bplist
|
||||||
# todo: check for None?
|
|
||||||
self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist)
|
self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist)
|
||||||
self._process_place_info()
|
self._process_place_info()
|
||||||
|
|
||||||
@@ -541,17 +534,21 @@ class PlaceInfo5(PlaceInfo):
|
|||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
addr = self._plrevgeoloc.postalAddress
|
addr = self._plrevgeoloc.postalAddress
|
||||||
address = PostalAddress(
|
if addr is not None:
|
||||||
street=addr._street,
|
postal_address = PostalAddress(
|
||||||
sub_locality=addr._subLocality,
|
street=addr._street,
|
||||||
city=addr._city,
|
sub_locality=addr._subLocality,
|
||||||
sub_administrative_area=addr._subAdministrativeArea,
|
city=addr._city,
|
||||||
state_province=addr._state,
|
sub_administrative_area=addr._subAdministrativeArea,
|
||||||
postal_code=addr._postalCode,
|
state_province=addr._state,
|
||||||
country=addr._country,
|
postal_code=addr._postalCode,
|
||||||
iso_country_code=addr._ISOCountryCode,
|
country=addr._country,
|
||||||
)
|
iso_country_code=addr._ISOCountryCode,
|
||||||
return address
|
)
|
||||||
|
else:
|
||||||
|
postal_address = None
|
||||||
|
|
||||||
|
return postal_address
|
||||||
|
|
||||||
def _process_place_info(self):
|
def _process_place_info(self):
|
||||||
""" Process sortedPlaceInfos to set self._name and self._names """
|
""" Process sortedPlaceInfos to set self._name and self._names """
|
||||||
@@ -630,16 +627,14 @@ class PlaceInfo5(PlaceInfo):
|
|||||||
"address_str": self.address_str,
|
"address_str": self.address_str,
|
||||||
"address": str(self.address),
|
"address": str(self.address),
|
||||||
}
|
}
|
||||||
strval = "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
||||||
return strval
|
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
info = {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"names": self.names._asdict(),
|
"names": self.names._asdict(),
|
||||||
"country_code": self.country_code,
|
"country_code": self.country_code,
|
||||||
"ishome": self.ishome,
|
"ishome": self.ishome,
|
||||||
"address_str": self.address_str,
|
"address_str": self.address_str,
|
||||||
"address": self.address._asdict(),
|
"address": self.address._asdict() if self.address is not None else None,
|
||||||
}
|
}
|
||||||
return info
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ def dd_to_dms_str(lat, lon):
|
|||||||
|
|
||||||
def get_system_library_path():
|
def get_system_library_path():
|
||||||
""" return the path to the system Photos library as string """
|
""" return the path to the system Photos library as string """
|
||||||
""" only works on MacOS 10.15+ """
|
""" only works on MacOS 10.15 """
|
||||||
""" on earlier versions, returns None """
|
""" on earlier versions, returns None """
|
||||||
_, major, _ = _get_os_version()
|
_, major, _ = _get_os_version()
|
||||||
if int(major) < 15:
|
if int(major) < 15:
|
||||||
@@ -166,16 +166,10 @@ def get_system_library_path():
|
|||||||
with open(plist_file, "rb") as fp:
|
with open(plist_file, "rb") as fp:
|
||||||
pl = plistload(fp)
|
pl = plistload(fp)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"could not find plist file: {str(plist_file)}")
|
logging.debug(f"could not find plist file: {str(plist_file)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
photospath = pl["SystemLibraryPath"]
|
return pl.get("SystemLibraryPath")
|
||||||
|
|
||||||
if photospath is not None:
|
|
||||||
return photospath
|
|
||||||
else:
|
|
||||||
logging.warning("Could not get path to Photos database")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_library_path():
|
def get_last_library_path():
|
||||||
@@ -194,7 +188,7 @@ def get_last_library_path():
|
|||||||
|
|
||||||
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
|
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
|
||||||
# this is a serialized CFData object
|
# this is a serialized CFData object
|
||||||
photosurlref = pl["IPXDefaultLibraryURLBookmark"]
|
photosurlref = pl.get("IPXDefaultLibraryURLBookmark")
|
||||||
|
|
||||||
if photosurlref is not None:
|
if photosurlref is not None:
|
||||||
# use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef
|
# use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef
|
||||||
@@ -261,10 +255,9 @@ def get_preferred_uti_extension(uti):
|
|||||||
|
|
||||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||||
|
|
||||||
ext = CoreServices.UTTypeCopyPreferredTagWithClass(
|
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||||
uti, CoreServices.kUTTagClassFilenameExtension
|
uti, CoreServices.kUTTagClassFilenameExtension
|
||||||
)
|
)
|
||||||
return ext
|
|
||||||
|
|
||||||
|
|
||||||
def findfiles(pattern, path_):
|
def findfiles(pattern, path_):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
|||||||
<key>hostuuid</key>
|
<key>hostuuid</key>
|
||||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||||
<key>pid</key>
|
<key>pid</key>
|
||||||
<integer>4021</integer>
|
<integer>703</integer>
|
||||||
<key>processname</key>
|
<key>processname</key>
|
||||||
<string>photolibraryd</string>
|
<string>photolibraryd</string>
|
||||||
<key>uid</key>
|
<key>uid</key>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,24 +3,24 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BackgroundHighlightCollection</key>
|
<key>BackgroundHighlightCollection</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-06T14:26:31Z</date>
|
||||||
<key>BackgroundHighlightEnrichment</key>
|
<key>BackgroundHighlightEnrichment</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-06T14:26:29Z</date>
|
||||||
<key>BackgroundJobAssetRevGeocode</key>
|
<key>BackgroundJobAssetRevGeocode</key>
|
||||||
<date>2020-05-30T04:01:24Z</date>
|
<date>2020-06-06T14:26:31Z</date>
|
||||||
<key>BackgroundJobSearch</key>
|
<key>BackgroundJobSearch</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-06T14:26:31Z</date>
|
||||||
<key>BackgroundPeopleSuggestion</key>
|
<key>BackgroundPeopleSuggestion</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-06T14:26:29Z</date>
|
||||||
<key>BackgroundUserBehaviorProcessor</key>
|
<key>BackgroundUserBehaviorProcessor</key>
|
||||||
<date>2020-05-29T04:31:38Z</date>
|
<date>2020-06-06T14:26:31Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||||
<date>2020-05-30T02:16:06Z</date>
|
<date>2020-05-30T02:16:06Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||||
<date>2020-05-29T04:31:37Z</date>
|
<date>2020-05-29T04:31:37Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||||
<date>2020-05-30T04:01:24Z</date>
|
<date>2020-06-06T14:26:33Z</date>
|
||||||
<key>SiriPortraitDonation</key>
|
<key>SiriPortraitDonation</key>
|
||||||
<date>2020-05-29T04:31:38Z</date>
|
<date>2020-06-06T14:26:31Z</date>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/test-images/IMG_1693.tif
Normal file
BIN
tests/test-images/IMG_1693.tif
Normal file
Binary file not shown.
@@ -16,7 +16,7 @@ KEYWORDS = [
|
|||||||
"United Kingdom",
|
"United Kingdom",
|
||||||
]
|
]
|
||||||
PERSONS = ["Katie", "Suzy", "Maria"]
|
PERSONS = ["Katie", "Suzy", "Maria"]
|
||||||
ALBUMS = ["Pumpkin Farm", "Last Import", "AlbumInFolder"]
|
ALBUMS = ["Pumpkin Farm", "AlbumInFolder"]
|
||||||
KEYWORDS_DICT = {
|
KEYWORDS_DICT = {
|
||||||
"Kids": 4,
|
"Kids": 4,
|
||||||
"wedding": 2,
|
"wedding": 2,
|
||||||
@@ -29,7 +29,7 @@ KEYWORDS_DICT = {
|
|||||||
"United Kingdom": 1,
|
"United Kingdom": 1,
|
||||||
}
|
}
|
||||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
|
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
|
||||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Last Import": 1, "AlbumInFolder": 1}
|
ALBUM_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 1}
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
@@ -124,7 +124,7 @@ def test_attributes():
|
|||||||
)
|
)
|
||||||
assert p.description == "Girl holding pumpkin"
|
assert p.description == "Girl holding pumpkin"
|
||||||
assert p.title == "I found one!"
|
assert p.title == "I found one!"
|
||||||
assert p.albums == ["Pumpkin Farm", "AlbumInFolder"]
|
assert sorted(p.albums) == ["AlbumInFolder", "Pumpkin Farm"]
|
||||||
assert p.persons == ["Katie"]
|
assert p.persons == ["Katie"]
|
||||||
assert p.path.endswith(
|
assert p.path.endswith(
|
||||||
"/tests/Test-10.12.6.photoslibrary/Masters/2019/08/24/20190824-030824/Pumkins2.jpg"
|
"/tests/Test-10.12.6.photoslibrary/Masters/2019/08/24/20190824-030824/Pumkins2.jpg"
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ def test_albums_photos():
|
|||||||
|
|
||||||
|
|
||||||
def test_photoinfo_albums():
|
def test_photoinfo_albums():
|
||||||
|
""" Test PhotoInfo.albums """
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
@@ -238,7 +239,20 @@ def test_photoinfo_albums():
|
|||||||
assert "Pumpkin Farm" in albums
|
assert "Pumpkin Farm" in albums
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_albums_2():
|
||||||
|
""" Test that PhotoInfo.albums returns only number albums expected """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["two_albums"]])
|
||||||
|
|
||||||
|
albums = photos[0].albums
|
||||||
|
assert len(albums) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_photoinfo_album_info():
|
def test_photoinfo_album_info():
|
||||||
|
""" test PhotoInfo.album_info """
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
@@ -249,4 +263,4 @@ def test_photoinfo_album_info():
|
|||||||
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
||||||
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
||||||
|
|
||||||
assert photos[0] in album_info[0].photos
|
assert photos[0].uuid in [photo.uuid for photo in album_info[0].photos]
|
||||||
|
|||||||
@@ -244,4 +244,4 @@ def test_photoinfo_album_info():
|
|||||||
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
||||||
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
||||||
|
|
||||||
assert photos[0] in album_info[0].photos
|
assert photos[0].uuid in [photo.uuid for photo in album_info[0].photos]
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ def test_init4():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_init5(mocker):
|
def test_init5(mocker):
|
||||||
# test failed get_last_library_path
|
# test failed get_last_library_path
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -116,7 +117,6 @@ def test_init5(mocker):
|
|||||||
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
|
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
|
||||||
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
|
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
|
||||||
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
|
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
|
||||||
|
|
||||||
|
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
assert osxphotos.PhotosDB()
|
assert osxphotos.PhotosDB()
|
||||||
@@ -207,7 +207,7 @@ def test_attributes():
|
|||||||
)
|
)
|
||||||
assert p.description == "Girl holding pumpkin"
|
assert p.description == "Girl holding pumpkin"
|
||||||
assert p.title == "I found one!"
|
assert p.title == "I found one!"
|
||||||
assert p.albums == ["Pumpkin Farm", "Test Album", "Multi Keyword"]
|
assert sorted(p.albums) == ["Multi Keyword", "Pumpkin Farm", "Test Album"]
|
||||||
assert p.persons == ["Katie"]
|
assert p.persons == ["Katie"]
|
||||||
assert p.path.endswith(
|
assert p.path.endswith(
|
||||||
"tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
"tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ def test_attributes():
|
|||||||
)
|
)
|
||||||
assert p.description == "Girl holding pumpkin"
|
assert p.description == "Girl holding pumpkin"
|
||||||
assert p.title == "I found one!"
|
assert p.title == "I found one!"
|
||||||
assert p.albums == ["Pumpkin Farm", "Test Album"]
|
assert sorted(p.albums) == ["Pumpkin Farm", "Test Album"]
|
||||||
assert p.persons == ["Katie"]
|
assert p.persons == ["Katie"]
|
||||||
assert p.path.endswith(
|
assert p.path.endswith(
|
||||||
"tests/Test-10.15.4.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
"tests/Test-10.15.4.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ PHOTOS_DB = "tests/Test-10.15.5.photoslibrary/database/photos.db"
|
|||||||
PHOTOS_DB_PATH = "/Test-10.15.5.photoslibrary/database/photos.db"
|
PHOTOS_DB_PATH = "/Test-10.15.5.photoslibrary/database/photos.db"
|
||||||
PHOTOS_LIBRARY_PATH = "/Test-10.15.5.photoslibrary"
|
PHOTOS_LIBRARY_PATH = "/Test-10.15.5.photoslibrary"
|
||||||
|
|
||||||
|
PHOTOS_DB_LEN = 13
|
||||||
|
|
||||||
KEYWORDS = [
|
KEYWORDS = [
|
||||||
"Kids",
|
"Kids",
|
||||||
"wedding",
|
"wedding",
|
||||||
@@ -22,10 +24,12 @@ KEYWORDS = [
|
|||||||
PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
|
PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
|
||||||
ALBUMS = [
|
ALBUMS = [
|
||||||
"Pumpkin Farm",
|
"Pumpkin Farm",
|
||||||
"Test Album",
|
"Test Album", # there are 2 albums named "Test Album" for testing duplicate album names
|
||||||
"AlbumInFolder",
|
"AlbumInFolder",
|
||||||
"Raw"
|
"Raw",
|
||||||
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
"I have a deleted twin", # there's an empty album with same name that has been deleted
|
||||||
|
"EmptyAlbum",
|
||||||
|
]
|
||||||
KEYWORDS_DICT = {
|
KEYWORDS_DICT = {
|
||||||
"Kids": 4,
|
"Kids": 4,
|
||||||
"wedding": 2,
|
"wedding": 2,
|
||||||
@@ -43,6 +47,8 @@ ALBUM_DICT = {
|
|||||||
"Test Album": 2,
|
"Test Album": 2,
|
||||||
"AlbumInFolder": 2,
|
"AlbumInFolder": 2,
|
||||||
"Raw": 4,
|
"Raw": 4,
|
||||||
|
"I have a deleted twin": 1,
|
||||||
|
"EmptyAlbum": 0,
|
||||||
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||||
|
|
||||||
UUID_DICT = {
|
UUID_DICT = {
|
||||||
@@ -58,8 +64,16 @@ UUID_DICT = {
|
|||||||
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||||
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||||
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||||
|
"export_tif": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||||
|
"in_album": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UUID_PUMPKIN_FARM = [
|
||||||
|
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||||
|
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||||
|
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_init1():
|
def test_init1():
|
||||||
# test named argument
|
# test named argument
|
||||||
@@ -109,14 +123,14 @@ def test_init4():
|
|||||||
def test_init5(mocker):
|
def test_init5(mocker):
|
||||||
# test failed get_last_library_path
|
# test failed get_last_library_path
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
def bad_library():
|
def bad_library():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
|
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
|
||||||
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
|
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
|
||||||
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
|
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
|
||||||
|
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
assert osxphotos.PhotosDB()
|
assert osxphotos.PhotosDB()
|
||||||
|
|
||||||
@@ -126,7 +140,7 @@ def test_db_len():
|
|||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
||||||
assert len(photosdb) == 12
|
assert len(photosdb) == PHOTOS_DB_LEN
|
||||||
|
|
||||||
|
|
||||||
def test_db_version():
|
def test_db_version():
|
||||||
@@ -214,7 +228,7 @@ def test_attributes():
|
|||||||
)
|
)
|
||||||
assert p.description == "Girl holding pumpkin"
|
assert p.description == "Girl holding pumpkin"
|
||||||
assert p.title == "I found one!"
|
assert p.title == "I found one!"
|
||||||
assert p.albums == ["Pumpkin Farm", "Test Album"]
|
assert sorted(p.albums) == ["Pumpkin Farm", "Test Album"]
|
||||||
assert p.persons == ["Katie"]
|
assert p.persons == ["Katie"]
|
||||||
assert p.path.endswith(
|
assert p.path.endswith(
|
||||||
"tests/Test-10.15.5.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
"tests/Test-10.15.5.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
||||||
@@ -229,7 +243,7 @@ def test_missing():
|
|||||||
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
|
||||||
assert len(photos) == 1
|
assert len(photos) == 1
|
||||||
p = photos[0]
|
p = photos[0]
|
||||||
assert p.path == None
|
assert p.path is None
|
||||||
assert p.ismissing == True
|
assert p.ismissing == True
|
||||||
|
|
||||||
|
|
||||||
@@ -378,7 +392,7 @@ def test_count():
|
|||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photos = photosdb.photos()
|
photos = photosdb.photos()
|
||||||
assert len(photos) == 12
|
assert len(photos) == PHOTOS_DB_LEN
|
||||||
|
|
||||||
|
|
||||||
def test_keyword_2():
|
def test_keyword_2():
|
||||||
@@ -402,6 +416,16 @@ def test_keyword_not_in_album():
|
|||||||
assert photos3[0].uuid == "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"
|
assert photos3[0].uuid == "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_folder_name():
|
||||||
|
"""Test query with album name same as a folder name """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
|
||||||
|
photos = photosdb.photos(albums=["Pumpkin Farm"])
|
||||||
|
assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM)
|
||||||
|
|
||||||
|
|
||||||
def test_get_db_path():
|
def test_get_db_path():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@@ -418,6 +442,26 @@ def test_get_library_path():
|
|||||||
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)
|
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_db_connection():
|
||||||
|
""" Test PhotosDB.get_db_connection """
|
||||||
|
import osxphotos
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
conn, cursor = photosdb.get_db_connection()
|
||||||
|
|
||||||
|
assert isinstance(conn, sqlite3.Connection)
|
||||||
|
assert isinstance(cursor, sqlite3.Cursor)
|
||||||
|
|
||||||
|
results = conn.execute(
|
||||||
|
"SELECT ZUUID FROM ZGENERICASSET WHERE ZFAVORITE = 1;"
|
||||||
|
).fetchall()
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0][0] == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # uuid
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_export_1():
|
def test_export_1():
|
||||||
# test basic export
|
# test basic export
|
||||||
# get an unedited image and export it using default filename
|
# get an unedited image and export it using default filename
|
||||||
@@ -730,12 +774,55 @@ def test_export_13():
|
|||||||
assert e.type == type(FileNotFoundError())
|
assert e.type == type(FileNotFoundError())
|
||||||
|
|
||||||
|
|
||||||
def test_eq():
|
def test_export_14(caplog):
|
||||||
|
# test export with user provided filename with different (but valid) extension than source
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
|
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
|
dest = tempdir.name
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photos1 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["export_tif"]])
|
||||||
photos2 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
|
||||||
|
timestamp = time.time()
|
||||||
|
filename = f"osxphotos-export-2-test-{timestamp}.tif"
|
||||||
|
expected_dest = os.path.join(dest, filename)
|
||||||
|
got_dest = photos[0].export(dest, filename)[0]
|
||||||
|
|
||||||
|
assert got_dest == expected_dest
|
||||||
|
assert os.path.isfile(got_dest)
|
||||||
|
|
||||||
|
assert "Invalid destination suffix" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq():
|
||||||
|
""" Test equality of two PhotoInfo objects """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb1 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos1 = photosdb1.photos(uuid=[UUID_DICT["export"]])
|
||||||
|
photos2 = photosdb2.photos(uuid=[UUID_DICT["export"]])
|
||||||
|
assert photos1[0] == photos2[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq_2():
|
||||||
|
""" Test equality of two PhotoInfo objects when one has memoized property """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb1 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos1 = photosdb1.photos(uuid=[UUID_DICT["in_album"]])
|
||||||
|
photos2 = photosdb2.photos(uuid=[UUID_DICT["in_album"]])
|
||||||
|
|
||||||
|
# memoize a value
|
||||||
|
albums = photos1[0].albums
|
||||||
|
assert albums
|
||||||
|
|
||||||
assert photos1[0] == photos2[0]
|
assert photos1[0] == photos2[0]
|
||||||
|
|
||||||
|
|
||||||
@@ -781,7 +868,7 @@ def test_from_to_date():
|
|||||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||||
|
|
||||||
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
|
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
|
||||||
assert len(photos) ==6
|
assert len(photos) == 7
|
||||||
|
|
||||||
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
|
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
|
||||||
assert len(photos) == 6
|
assert len(photos) == 6
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ CLI_OUTPUT_NO_SUBCOMMAND = [
|
|||||||
" help Print help; for help on commands: help <command>.",
|
" help Print help; for help on commands: help <command>.",
|
||||||
" info Print out descriptive info of the Photos library database.",
|
" info Print out descriptive info of the Photos library database.",
|
||||||
" keywords Print out keywords found in the Photos library.",
|
" 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.",
|
" list Print list of Photos libraries found on the system.",
|
||||||
" persons Print out persons (faces) found in the Photos library.",
|
" persons Print out persons (faces) found in the Photos library.",
|
||||||
" places Print out places found in the Photos library.",
|
" places Print out places found in the Photos library.",
|
||||||
@@ -49,6 +50,10 @@ CLI_EXPORT_FILENAMES = [
|
|||||||
"wedding_edited.jpeg",
|
"wedding_edited.jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CLI_EXPORT_FILENAMES_ALBUM = ["Pumkins1.jpg", "Pumkins2.jpg", "Pumpkins3.jpg"]
|
||||||
|
|
||||||
|
CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"]
|
||||||
|
|
||||||
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
|
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
||||||
@@ -164,6 +169,9 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
|
|||||||
"Pumpkin Farm-Pumpkins3.jpg",
|
"Pumpkin Farm-Pumpkins3.jpg",
|
||||||
"Test Album-Pumkins1.jpg",
|
"Test Album-Pumkins1.jpg",
|
||||||
"Test Album-Pumkins2.jpg",
|
"Test Album-Pumkins2.jpg",
|
||||||
|
"None-IMG_1693.tif",
|
||||||
|
"I have a deleted twin-wedding.jpg",
|
||||||
|
"I have a deleted twin-wedding_edited.jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B"
|
CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B"
|
||||||
@@ -203,6 +211,63 @@ CLI_EXIFTOOL = {
|
|||||||
"XMP:Subject": ["Kids", "Katie"],
|
"XMP:Subject": ["Kids", "Katie"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LABELS_JSON = {
|
||||||
|
"labels": {
|
||||||
|
"Plant": 5,
|
||||||
|
"Tree": 2,
|
||||||
|
"Sky": 2,
|
||||||
|
"Outdoor": 2,
|
||||||
|
"Art": 2,
|
||||||
|
"Foliage": 2,
|
||||||
|
"Waterways": 1,
|
||||||
|
"River": 1,
|
||||||
|
"Cloudy": 1,
|
||||||
|
"Land": 1,
|
||||||
|
"Water Body": 1,
|
||||||
|
"Water": 1,
|
||||||
|
"Statue": 1,
|
||||||
|
"Window": 1,
|
||||||
|
"Decorative Plant": 1,
|
||||||
|
"Blue Sky": 1,
|
||||||
|
"Palm Tree": 1,
|
||||||
|
"Flower": 1,
|
||||||
|
"Flower Arrangement": 1,
|
||||||
|
"Bouquet": 1,
|
||||||
|
"Vase": 1,
|
||||||
|
"Container": 1,
|
||||||
|
"Camera": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KEYWORDS_JSON = {
|
||||||
|
"keywords": {
|
||||||
|
"Kids": 4,
|
||||||
|
"wedding": 2,
|
||||||
|
"London 2018": 1,
|
||||||
|
"St. James's Park": 1,
|
||||||
|
"England": 1,
|
||||||
|
"United Kingdom": 1,
|
||||||
|
"UK": 1,
|
||||||
|
"London": 1,
|
||||||
|
"flowers": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ALBUMS_JSON = {
|
||||||
|
"albums": {
|
||||||
|
"Raw": 4,
|
||||||
|
"Pumpkin Farm": 3,
|
||||||
|
"AlbumInFolder": 2,
|
||||||
|
"Test Album": 2,
|
||||||
|
"I have a deleted twin": 1,
|
||||||
|
"EmptyAlbum": 0,
|
||||||
|
},
|
||||||
|
"shared albums": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
PERSONS_JSON = {"persons": {"Katie": 3, "Suzy": 2, "_UNKNOWN_": 1, "Maria": 1}}
|
||||||
|
|
||||||
# determine if exiftool installed so exiftool tests can be skipped
|
# determine if exiftool installed so exiftool tests can be skipped
|
||||||
try:
|
try:
|
||||||
exiftool = get_exiftool_path()
|
exiftool = get_exiftool_path()
|
||||||
@@ -217,9 +282,10 @@ def test_osxphotos():
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
output = result.output
|
output = result.output
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
||||||
assert line in output
|
assert line.strip() in output
|
||||||
|
|
||||||
|
|
||||||
def test_osxphotos_help_1():
|
def test_osxphotos_help_1():
|
||||||
@@ -232,7 +298,7 @@ def test_osxphotos_help_1():
|
|||||||
output = result.output
|
output = result.output
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
||||||
assert line in output
|
assert line.strip() in output
|
||||||
|
|
||||||
|
|
||||||
def test_osxphotos_help_2():
|
def test_osxphotos_help_2():
|
||||||
@@ -242,7 +308,6 @@ def test_osxphotos_help_2():
|
|||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["help", "persons"])
|
result = runner.invoke(cli, ["help", "persons"])
|
||||||
output = result.output
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Print out persons (faces) found in the Photos library." in result.output
|
assert "Print out persons (faces) found in the Photos library." in result.output
|
||||||
|
|
||||||
@@ -254,7 +319,6 @@ def test_osxphotos_help_3():
|
|||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["help", "foo"])
|
result = runner.invoke(cli, ["help", "foo"])
|
||||||
output = result.output
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Invalid command: foo" in result.output
|
assert "Invalid command: foo" in result.output
|
||||||
|
|
||||||
@@ -291,7 +355,10 @@ def test_query_uuid():
|
|||||||
for key_ in json_expected:
|
for key_ in json_expected:
|
||||||
assert key_ in json_got
|
assert key_ in json_got
|
||||||
if key_ != "path":
|
if key_ != "path":
|
||||||
assert json_expected[key_] == json_got[key_]
|
if isinstance(json_expected[key_], list):
|
||||||
|
assert sorted(json_expected[key_]) == sorted(json_got[key_])
|
||||||
|
else:
|
||||||
|
assert json_expected[key_] == json_got[key_]
|
||||||
else:
|
else:
|
||||||
assert json_expected[key_] in json_got[key_]
|
assert json_expected[key_] in json_got[key_]
|
||||||
|
|
||||||
@@ -509,14 +576,387 @@ def test_query_date():
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.warning(result.output)
|
|
||||||
|
|
||||||
json_got = json.loads(result.output)
|
json_got = json.loads(result.output)
|
||||||
assert len(json_got) == 4
|
assert len(json_got) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_keyword_1():
|
||||||
|
"""Test query --keyword """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--keyword", "Kids"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_keyword_2():
|
||||||
|
"""Test query --keyword with lower case keyword"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--keyword", "kids"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_keyword_3():
|
||||||
|
"""Test query --keyword with lower case keyword and --ignore-case"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--keyword",
|
||||||
|
"kids",
|
||||||
|
"--ignore-case",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_keyword_4():
|
||||||
|
"""Test query with more than one --keyword"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--keyword",
|
||||||
|
"Kids",
|
||||||
|
"--keyword",
|
||||||
|
"wedding",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_person_1():
|
||||||
|
"""Test query --person"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--person", "Katie"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_person_2():
|
||||||
|
"""Test query --person with lower case person"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--person", "katie"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_person_3():
|
||||||
|
"""Test query --person with lower case person and --ignore-case"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--person",
|
||||||
|
"katie",
|
||||||
|
"--ignore-case",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_person_4():
|
||||||
|
"""Test query with multiple --person"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--person",
|
||||||
|
"Katie",
|
||||||
|
"--person",
|
||||||
|
"Maria",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_album_1():
|
||||||
|
"""Test query --album"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--album",
|
||||||
|
"Pumpkin Farm",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_album_2():
|
||||||
|
"""Test query --album with lower case album"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--album",
|
||||||
|
"pumpkin farm",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_album_3():
|
||||||
|
"""Test query --album with lower case album and --ignore-case"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--album",
|
||||||
|
"pumpkin farm",
|
||||||
|
"--ignore-case",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_album_4():
|
||||||
|
"""Test query with multipl --album"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--album",
|
||||||
|
"Pumpkin Farm",
|
||||||
|
"--album",
|
||||||
|
"Raw",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_label_1():
|
||||||
|
"""Test query --label"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--label", "Statue"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_label_2():
|
||||||
|
"""Test query --label with lower case label """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--label", "statue"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_label_3():
|
||||||
|
"""Test query --label with lower case label and --ignore-case"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--label",
|
||||||
|
"statue",
|
||||||
|
"--ignore-case",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_label_4():
|
||||||
|
"""Test query with more than one --label"""
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--label",
|
||||||
|
"Statue",
|
||||||
|
"--label",
|
||||||
|
"Plant",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert len(json_got) == 6
|
||||||
|
|
||||||
|
|
||||||
def test_export_sidecar():
|
def test_export_sidecar():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -946,6 +1386,54 @@ def test_export_filename_template_3():
|
|||||||
assert "Error: Invalid template" in result.output
|
assert "Error: Invalid template" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_album():
|
||||||
|
"""Test export of an album """
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import export
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
export,
|
||||||
|
[os.path.join(cwd, PHOTOS_DB_15_5), ".", "--album", "Pumpkin Farm", "-V"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_ALBUM)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_album_deleted_twin():
|
||||||
|
"""Test export of an album where album of same name has been deleted """
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import export
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
export,
|
||||||
|
[
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
".",
|
||||||
|
"--album",
|
||||||
|
"I have a deleted twin",
|
||||||
|
"-V",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_DELETED_TWIN)
|
||||||
|
|
||||||
|
|
||||||
def test_places():
|
def test_places():
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -1205,8 +1693,6 @@ def test_export_sidecar_keyword_template():
|
|||||||
"EXIF:ModifyDate": "2020:04:11 12:34:16"}]"""
|
"EXIF:ModifyDate": "2020:04:11 12:34:16"}]"""
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
json_file = open("Pumkins2.json", "r")
|
json_file = open("Pumkins2.json", "r")
|
||||||
json_got = json.load(json_file)[0]
|
json_got = json.load(json_file)[0]
|
||||||
json_file.close()
|
json_file.close()
|
||||||
@@ -1256,6 +1742,59 @@ def test_export_update_basic():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_update_child_folder():
|
||||||
|
""" test export then update into a child folder of previous export """
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import export, OSXPHOTOS_EXPORT_DB
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# basic export
|
||||||
|
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
os.mkdir("foo")
|
||||||
|
|
||||||
|
# update into foo
|
||||||
|
result = runner.invoke(
|
||||||
|
export, [os.path.join(cwd, CLI_PHOTOS_DB), "foo", "--update"], input="N\n"
|
||||||
|
)
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "WARNING: found other export database files" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_update_parent_folder():
|
||||||
|
""" test export then update into a parent folder of previous export """
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import export, OSXPHOTOS_EXPORT_DB
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# basic export
|
||||||
|
os.mkdir("foo")
|
||||||
|
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), "foo", "-V"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# update into "."
|
||||||
|
result = runner.invoke(
|
||||||
|
export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--update"], input="N\n"
|
||||||
|
)
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "WARNING: found other export database files" in result.output
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
def test_export_update_exiftool():
|
def test_export_update_exiftool():
|
||||||
""" test export then update with exiftool """
|
""" test export then update with exiftool """
|
||||||
@@ -1571,3 +2110,79 @@ def test_export_directory_template_1_dry_run():
|
|||||||
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1:
|
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1:
|
||||||
assert f"Exported {filepath}" in result.output
|
assert f"Exported {filepath}" in result.output
|
||||||
assert not os.path.isfile(os.path.join(workdir, filepath))
|
assert not os.path.isfile(os.path.join(workdir, filepath))
|
||||||
|
|
||||||
|
|
||||||
|
def test_labels():
|
||||||
|
"""Test osxphotos labels """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import labels
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
labels, ["--db", os.path.join(cwd, PHOTOS_DB_15_5), "--json"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert json_got == LABELS_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def test_keywords():
|
||||||
|
"""Test osxphotos keywords """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import keywords
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
keywords, ["--db", os.path.join(cwd, PHOTOS_DB_15_5), "--json"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert json_got == KEYWORDS_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def test_albums():
|
||||||
|
"""Test osxphotos albums """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import albums
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
albums, ["--db", os.path.join(cwd, PHOTOS_DB_15_5), "--json"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert json_got == ALBUMS_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def test_persons():
|
||||||
|
"""Test osxphotos albums """
|
||||||
|
import json
|
||||||
|
import osxphotos
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from osxphotos.__main__ import persons
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
persons, ["--db", os.path.join(cwd, PHOTOS_DB_15_5), "--json"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
assert json_got == PERSONS_JSON
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
""" test datetime_formatter.DateTimeFormatter """
|
""" test datetime_formatter.DateTimeFormatter """
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
def test_datetime_formatter():
|
|
||||||
|
def test_datetime_formatter_1():
|
||||||
|
"""Test DateTimeFormatter """
|
||||||
import datetime
|
import datetime
|
||||||
import locale
|
import locale
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
dt = datetime.datetime(2020,5,23)
|
dt = datetime.datetime(2020, 5, 23, 12, 42, 33)
|
||||||
dtf = DateTimeFormatter(dt)
|
dtf = DateTimeFormatter(dt)
|
||||||
|
|
||||||
assert dtf.date == "2020-05-23"
|
assert dtf.date == "2020-05-23"
|
||||||
@@ -19,3 +21,54 @@ def test_datetime_formatter():
|
|||||||
assert dtf.mm == "05"
|
assert dtf.mm == "05"
|
||||||
assert dtf.dd == "23"
|
assert dtf.dd == "23"
|
||||||
assert dtf.doy == "144"
|
assert dtf.doy == "144"
|
||||||
|
assert dtf.hour == "12"
|
||||||
|
assert dtf.min == "42"
|
||||||
|
assert dtf.sec == "33"
|
||||||
|
|
||||||
|
|
||||||
|
def test_datetime_formatter_2():
|
||||||
|
"""Test DateTimeFormatter with hour > 12 """
|
||||||
|
import datetime
|
||||||
|
import locale
|
||||||
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
|
dt = datetime.datetime(2020, 5, 23, 14, 42, 33)
|
||||||
|
dtf = DateTimeFormatter(dt)
|
||||||
|
|
||||||
|
assert dtf.date == "2020-05-23"
|
||||||
|
assert dtf.year == "2020"
|
||||||
|
assert dtf.yy == "20"
|
||||||
|
assert dtf.month == "May"
|
||||||
|
assert dtf.mon == "May"
|
||||||
|
assert dtf.mm == "05"
|
||||||
|
assert dtf.dd == "23"
|
||||||
|
assert dtf.doy == "144"
|
||||||
|
assert dtf.hour == "14"
|
||||||
|
assert dtf.min == "42"
|
||||||
|
assert dtf.sec == "33"
|
||||||
|
|
||||||
|
|
||||||
|
def test_datetime_formatter_3():
|
||||||
|
"""Test DateTimeFormatter zero-padding """
|
||||||
|
import datetime
|
||||||
|
import locale
|
||||||
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
|
dt = datetime.datetime(2020, 5, 2, 9, 3, 6)
|
||||||
|
dtf = DateTimeFormatter(dt)
|
||||||
|
|
||||||
|
assert dtf.date == "2020-05-02"
|
||||||
|
assert dtf.year == "2020"
|
||||||
|
assert dtf.yy == "20"
|
||||||
|
assert dtf.month == "May"
|
||||||
|
assert dtf.mon == "May"
|
||||||
|
assert dtf.mm == "05"
|
||||||
|
assert dtf.dd == "02"
|
||||||
|
assert dtf.doy == "123"
|
||||||
|
assert dtf.hour == "09"
|
||||||
|
assert dtf.min == "03"
|
||||||
|
assert dtf.sec == "06"
|
||||||
|
|||||||
97
tests/test_score_info.py
Normal file
97
tests/test_score_info.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
""" Test ScoreInfo """
|
||||||
|
|
||||||
|
from math import isclose
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from osxphotos.photoinfo import ScoreInfo
|
||||||
|
|
||||||
|
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
|
||||||
|
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
||||||
|
|
||||||
|
SCORE_DICT = {
|
||||||
|
"4D521201-92AC-43E5-8F7C-59BC41C37A96": ScoreInfo(
|
||||||
|
overall=0.470703125,
|
||||||
|
curation=0.5,
|
||||||
|
promotion=0.0,
|
||||||
|
highlight_visibility=0.03816793893129771,
|
||||||
|
behavioral=0.0,
|
||||||
|
failure=-0.0006928443908691406,
|
||||||
|
harmonious_color=0.017852783203125,
|
||||||
|
immersiveness=0.003086090087890625,
|
||||||
|
interaction=0.019999999552965164,
|
||||||
|
interesting_subject=-0.0885009765625,
|
||||||
|
intrusive_object_presence=-0.037872314453125,
|
||||||
|
lively_color=0.10540771484375,
|
||||||
|
low_light=0.00824737548828125,
|
||||||
|
noise=-0.015655517578125,
|
||||||
|
pleasant_camera_tilt=-0.006256103515625,
|
||||||
|
pleasant_composition=0.028564453125,
|
||||||
|
pleasant_lighting=-0.00439453125,
|
||||||
|
pleasant_pattern=0.09088134765625,
|
||||||
|
pleasant_perspective=0.11859130859375,
|
||||||
|
pleasant_post_processing=0.00698089599609375,
|
||||||
|
pleasant_reflection=-0.01523590087890625,
|
||||||
|
pleasant_symmetry=0.01242828369140625,
|
||||||
|
sharply_focused_subject=0.08538818359375,
|
||||||
|
tastefully_blurred=0.022125244140625,
|
||||||
|
well_chosen_subject=0.05596923828125,
|
||||||
|
well_framed_subject=0.5986328125,
|
||||||
|
well_timed_shot=0.0134124755859375,
|
||||||
|
),
|
||||||
|
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": ScoreInfo(
|
||||||
|
overall=0.853515625,
|
||||||
|
curation=0.75,
|
||||||
|
promotion=0.0,
|
||||||
|
highlight_visibility=0.05725190839694656,
|
||||||
|
behavioral=0.0,
|
||||||
|
failure=-0.0004916191101074219,
|
||||||
|
harmonious_color=0.382080078125,
|
||||||
|
immersiveness=0.0133209228515625,
|
||||||
|
interaction=0.03999999910593033,
|
||||||
|
interesting_subject=0.1632080078125,
|
||||||
|
intrusive_object_presence=-0.00966644287109375,
|
||||||
|
lively_color=0.44091796875,
|
||||||
|
low_light=0.01322174072265625,
|
||||||
|
noise=-0.0026721954345703125,
|
||||||
|
pleasant_camera_tilt=0.028045654296875,
|
||||||
|
pleasant_composition=0.33642578125,
|
||||||
|
pleasant_lighting=0.46142578125,
|
||||||
|
pleasant_pattern=0.1944580078125,
|
||||||
|
pleasant_perspective=0.494384765625,
|
||||||
|
pleasant_post_processing=0.4970703125,
|
||||||
|
pleasant_reflection=0.00910186767578125,
|
||||||
|
pleasant_symmetry=0.00930023193359375,
|
||||||
|
sharply_focused_subject=0.52490234375,
|
||||||
|
tastefully_blurred=0.63916015625,
|
||||||
|
well_chosen_subject=0.64208984375,
|
||||||
|
well_framed_subject=0.485595703125,
|
||||||
|
well_timed_shot=0.01531219482421875,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def photosdb():
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_info_v5(photosdb):
|
||||||
|
""" test score """
|
||||||
|
# use math.isclose to compare floats
|
||||||
|
# on MacOS x64 these can probably compared for equality but would possibly
|
||||||
|
# fail if osxphotos ever ported to other platforms
|
||||||
|
for uuid in SCORE_DICT:
|
||||||
|
photo = photosdb.photos(uuid=[uuid], movies=True)[0]
|
||||||
|
for attr in photo.score.__dict__:
|
||||||
|
assert isclose(getattr(photo.score, attr), getattr(SCORE_DICT[uuid], attr))
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_info_v4():
|
||||||
|
""" test version 4, score should be None """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
|
||||||
|
for photo in photosdb.photos():
|
||||||
|
assert photo.score is None
|
||||||
@@ -32,6 +32,9 @@ TEMPLATE_VALUES = {
|
|||||||
"{created.dd}": "04",
|
"{created.dd}": "04",
|
||||||
"{created.dow}": "Tuesday",
|
"{created.dow}": "Tuesday",
|
||||||
"{created.doy}": "035",
|
"{created.doy}": "035",
|
||||||
|
"{created.hour}": "19",
|
||||||
|
"{created.min}": "07",
|
||||||
|
"{created.sec}": "38",
|
||||||
"{modified.date}": "2020-03-21",
|
"{modified.date}": "2020-03-21",
|
||||||
"{modified.year}": "2020",
|
"{modified.year}": "2020",
|
||||||
"{modified.yy}": "20",
|
"{modified.yy}": "20",
|
||||||
@@ -40,6 +43,9 @@ TEMPLATE_VALUES = {
|
|||||||
"{modified.mon}": "Mar",
|
"{modified.mon}": "Mar",
|
||||||
"{modified.dd}": "21",
|
"{modified.dd}": "21",
|
||||||
"{modified.doy}": "081",
|
"{modified.doy}": "081",
|
||||||
|
"{modified.hour}": "01",
|
||||||
|
"{modified.min}": "33",
|
||||||
|
"{modified.sec}": "08",
|
||||||
"{place.name}": "Washington, District of Columbia, United States",
|
"{place.name}": "Washington, District of Columbia, United States",
|
||||||
"{place.country_code}": "US",
|
"{place.country_code}": "US",
|
||||||
"{place.name.country}": "United States",
|
"{place.name.country}": "United States",
|
||||||
@@ -106,7 +112,7 @@ def test_lookup():
|
|||||||
|
|
||||||
for subst in TEMPLATE_SUBSTITUTIONS:
|
for subst in TEMPLATE_SUBSTITUTIONS:
|
||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
lookup = template.get_template_value(lookup_str)
|
lookup = template.get_template_value(lookup_str, None)
|
||||||
assert lookup or lookup is None
|
assert lookup or lookup is None
|
||||||
|
|
||||||
|
|
||||||
@@ -115,7 +121,10 @@ def test_lookup_multi():
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.phototemplate import TEMPLATE_SUBSTITUTIONS_MULTI_VALUED, PhotoTemplate
|
from osxphotos.phototemplate import (
|
||||||
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
|
PhotoTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
@@ -123,10 +132,11 @@ def test_lookup_multi():
|
|||||||
|
|
||||||
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
lookup = template.get_template_value_multi(lookup_str,path_sep=os.path.sep)
|
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
||||||
assert isinstance(lookup, list)
|
assert isinstance(lookup, list)
|
||||||
assert len(lookup) >= 1
|
assert len(lookup) >= 1
|
||||||
|
|
||||||
|
|
||||||
def test_subst():
|
def test_subst():
|
||||||
""" Test that substitutions are correct """
|
""" Test that substitutions are correct """
|
||||||
import locale
|
import locale
|
||||||
@@ -432,3 +442,19 @@ def test_subst_multi_folder_albums_3():
|
|||||||
rendered, unknown = photo.render_template(template)
|
rendered, unknown = photo.render_template(template)
|
||||||
assert sorted(rendered) == sorted(expected)
|
assert sorted(rendered) == sorted(expected)
|
||||||
assert unknown == []
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_strftime():
|
||||||
|
""" Test that strftime substitutions are correct """
|
||||||
|
import locale
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
|
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
|
||||||
|
assert rendered[0] == "2020-02-04-190738"
|
||||||
|
|
||||||
|
rendered, unmatched = photo.render_template("{created.strftime}")
|
||||||
|
assert rendered[0] == "_"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
""" Builds the template table in markdown format for README.md """
|
""" Builds the template table in markdown format for README.md """
|
||||||
|
|
||||||
from osxphotos.photoinfo.template import (
|
from osxphotos.phototemplate import (
|
||||||
TEMPLATE_SUBSTITUTIONS,
|
TEMPLATE_SUBSTITUTIONS,
|
||||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user