Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
185483e1aa | ||
|
|
c1d12047bd | ||
|
|
46c87eeed5 | ||
|
|
fd4c99032d | ||
|
|
d6fee89fd9 | ||
|
|
b8618cf272 | ||
|
|
6b7c5d07fd | ||
|
|
bd5ba702aa | ||
|
|
c8d76a89e4 | ||
|
|
a8e996e660 | ||
|
|
c68a5ab39f | ||
|
|
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 |
157
CHANGELOG.md
@@ -4,6 +4,103 @@ 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.28](https://github.com/RhetTbull/osxphotos/compare/v0.29.26...v0.29.28)
|
||||||
|
|
||||||
|
> 22 June 2020
|
||||||
|
|
||||||
|
- Closes #174 [`#174`](https://github.com/RhetTbull/osxphotos/issues/174)
|
||||||
|
- Added today to template system, closes #167 [`#167`](https://github.com/RhetTbull/osxphotos/issues/167)
|
||||||
|
- Minor refactoring in photoinfo.py [`a8e996e`](https://github.com/RhetTbull/osxphotos/commit/a8e996e66072e94de93fd4ea78a456bc61831f52)
|
||||||
|
|
||||||
|
#### [v0.29.26](https://github.com/RhetTbull/osxphotos/compare/v0.29.25...v0.29.26)
|
||||||
|
|
||||||
|
> 21 June 2020
|
||||||
|
|
||||||
|
- Bug fix for issue #172 [`1ebf995`](https://github.com/RhetTbull/osxphotos/commit/1ebf99583397617f0d3a234c898beae1c14f5a63)
|
||||||
|
|
||||||
|
#### [v0.29.25](https://github.com/RhetTbull/osxphotos/compare/v0.29.24...v0.29.25)
|
||||||
|
|
||||||
|
> 21 June 2020
|
||||||
|
|
||||||
|
- More PhotoInfo.albums refactoring, closes #169 [`#169`](https://github.com/RhetTbull/osxphotos/issues/169)
|
||||||
|
|
||||||
|
#### [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 +121,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 +135,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 +145,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 +155,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 +180,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 +205,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 +215,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 +222,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 +236,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 +244,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 +261,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 +274,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 +282,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 +291,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 +298,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 +317,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 +338,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 +354,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 +378,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 +393,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 +420,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 +432,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 +446,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 +458,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 +466,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 +481,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 +496,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 +512,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
|
||||||
|
|
||||||
|
|||||||
161
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,37 @@ 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
|
||||||
|
{today.date} Current date in iso format, e.g.
|
||||||
|
'2020-03-22'
|
||||||
|
{today.year} 4-digit year of current date
|
||||||
|
{today.yy} 2-digit year of current date
|
||||||
|
{today.mm} 2-digit month of the current date (zero
|
||||||
|
padded)
|
||||||
|
{today.month} Month name in user's locale of the current
|
||||||
|
date
|
||||||
|
{today.mon} Month abbreviation in the user's locale of
|
||||||
|
the current date
|
||||||
|
{today.dd} 2-digit day of the month (zero padded) of
|
||||||
|
current date
|
||||||
|
{today.dow} Day of week in user's locale of the current
|
||||||
|
date
|
||||||
|
{today.doy} 3-digit day of year (e.g Julian day) of
|
||||||
|
current date, starting from 1 (zero padded)
|
||||||
|
{today.hour} 2-digit hour of the current date
|
||||||
|
{today.min} 2-digit minute of the current date
|
||||||
|
{today.sec} 2-digit second of the current date
|
||||||
|
{today.strftime} Apply strftime template to current
|
||||||
|
date/time. Should be used in form
|
||||||
|
{today.strftime,TEMPLATE} where TEMPLATE is
|
||||||
|
a valid strftime template, e.g.
|
||||||
|
{today.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
|
{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,8 +857,25 @@ 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.
|
||||||
|
|
||||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
|
```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, intrash=False)`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# assumes photosdb is a PhotosDB object (see above)
|
# assumes photosdb is a PhotosDB object (see above)
|
||||||
@@ -829,7 +896,8 @@ photos = photosdb.photos(
|
|||||||
images = bool,
|
images = bool,
|
||||||
movies = bool,
|
movies = bool,
|
||||||
from_date = datetime.datetime,
|
from_date = datetime.datetime,
|
||||||
to_date = datetime.datetime
|
to_date = datetime.datetime,
|
||||||
|
intrash = bool,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -841,6 +909,7 @@ photos = photosdb.photos(
|
|||||||
- ```movies```: bool; if True, returns movies/videos; default is False
|
- ```movies```: bool; if True, returns movies/videos; default is False
|
||||||
- ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
|
- ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
|
||||||
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
||||||
|
- ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
|
||||||
|
|
||||||
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
||||||
|
|
||||||
@@ -980,6 +1049,9 @@ Returns `True` if the picture has been marked as a favorite, otherwise `False`
|
|||||||
#### `hidden`
|
#### `hidden`
|
||||||
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
||||||
|
|
||||||
|
#### `intrash`
|
||||||
|
Returns `True` if the picture is in the trash ('Recently Deleted' folder), otherwise `False`
|
||||||
|
|
||||||
#### `location`
|
#### `location`
|
||||||
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
||||||
|
|
||||||
@@ -1128,7 +1200,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,11 +1451,49 @@ 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
|
||||||
|
|
||||||
The following substitutions are availabe for use with `PhotoInfo.render_template()`
|
The following substitutions are availabe for use with `PhotoInfo.render_template()`
|
||||||
|
|
||||||
| Substitution | Description |
|
| Substitution | Description |
|
||||||
|--------------|-------------|
|
|--------------|-------------|
|
||||||
|{name}|Current filename of the photo|
|
|{name}|Current filename of the photo|
|
||||||
@@ -1392,7 +1507,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 +1521,22 @@ 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|
|
||||||
|
|{today.date}|Current date in iso format, e.g. '2020-03-22'|
|
||||||
|
|{today.year}|4-digit year of current date|
|
||||||
|
|{today.yy}|2-digit year of current date|
|
||||||
|
|{today.mm}|2-digit month of the current date (zero padded)|
|
||||||
|
|{today.month}|Month name in user's locale of the current date|
|
||||||
|
|{today.mon}|Month abbreviation in the user's locale of the current date|
|
||||||
|
|{today.dd}|2-digit day of the month (zero padded) of current date|
|
||||||
|
|{today.dow}|Day of week in user's locale of the current date|
|
||||||
|
|{today.doy}|3-digit day of year (e.g Julian day) of current date, starting from 1 (zero padded)|
|
||||||
|
|{today.hour}|2-digit hour of the current date|
|
||||||
|
|{today.min}|2-digit minute of the current date|
|
||||||
|
|{today.sec}|2-digit second of the current date|
|
||||||
|
|{today.strftime}|Apply strftime template to current date/time. Should be used in form {today.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {today.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|
|
||||||
@@ -1421,7 +1557,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()
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import logging
|
|||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .utils import _set_debug, _debug, _get_logger
|
from .phototemplate import PhotoTemplate
|
||||||
|
from .utils import _debug, _get_logger, _set_debug
|
||||||
|
|
||||||
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
|
||||||
# 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__ and to_json
|
# TODO: Add test for __str__ and to_json
|
||||||
# TODO: fix docstrings
|
|
||||||
# TODO: Add special albums and magic albums
|
# TODO: Add special albums and magic albums
|
||||||
# TODO: cleanup os.path and pathlib code (import pathlib and also from pathlib import Path)
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.30.0"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -64,6 +69,7 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
""" filename of the picture """
|
""" filename of the picture """
|
||||||
|
# sourcery off
|
||||||
if self.has_raw and self.raw_original:
|
if self.has_raw and self.raw_original:
|
||||||
# return name of the RAW file
|
# return name of the RAW file
|
||||||
# TODO: not yet implemented
|
# TODO: not yet implemented
|
||||||
@@ -84,8 +90,7 @@ class PhotoInfo:
|
|||||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
tz = timezone(delta)
|
tz = timezone(delta)
|
||||||
imagedate_utc = imagedate.astimezone(tz=tz)
|
return imagedate.astimezone(tz=tz)
|
||||||
return imagedate_utc
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_modified(self):
|
def date_modified(self):
|
||||||
@@ -96,8 +101,7 @@ class PhotoInfo:
|
|||||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
tz = timezone(delta)
|
tz = timezone(delta)
|
||||||
imagedate_utc = imagedate.astimezone(tz=tz)
|
return imagedate.astimezone(tz=tz)
|
||||||
return imagedate_utc
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -340,21 +344,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):
|
||||||
@@ -408,6 +417,11 @@ class PhotoInfo:
|
|||||||
""" True if picture is hidden """
|
""" True if picture is hidden """
|
||||||
return True if self._info["hidden"] == 1 else False
|
return True if self._info["hidden"] == 1 else False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def intrash(self):
|
||||||
|
""" True if picture is in trash ('Recently Deleted' folder)"""
|
||||||
|
return self._info["intrash"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location(self):
|
||||||
""" returns (latitude, longitude) as float in degrees or None """
|
""" returns (latitude, longitude) as float in degrees or None """
|
||||||
@@ -484,12 +498,11 @@ class PhotoInfo:
|
|||||||
self is not included in the returned list """
|
self is not included in the returned list """
|
||||||
if self._info["burst"]:
|
if self._info["burst"]:
|
||||||
burst_uuid = self._info["burstUUID"]
|
burst_uuid = self._info["burstUUID"]
|
||||||
burst_photos = [
|
return [
|
||||||
PhotoInfo(db=self._db, uuid=u, info=self._db._dbphotos[u])
|
PhotoInfo(db=self._db, uuid=u, info=self._db._dbphotos[u])
|
||||||
for u in self._db._dbphotos_burst[burst_uuid]
|
for u in self._db._dbphotos_burst[burst_uuid]
|
||||||
if u != self._uuid
|
if u != self._uuid
|
||||||
]
|
]
|
||||||
return burst_photos
|
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -637,6 +650,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 +667,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 +708,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 +750,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 +765,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 +811,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
@@ -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
|
||||||
@@ -727,9 +715,10 @@ class PhotosDB:
|
|||||||
RKVersion.specialType, RKMaster.modelID, null, RKVersion.momentUuid,
|
RKVersion.specialType, RKMaster.modelID, null, RKVersion.momentUuid,
|
||||||
RKVersion.rawMasterUuid,
|
RKVersion.rawMasterUuid,
|
||||||
RKVersion.nonRawMasterUuid,
|
RKVersion.nonRawMasterUuid,
|
||||||
RKMaster.alternateMasterUuid
|
RKMaster.alternateMasterUuid,
|
||||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
RKVersion.isInTrash
|
||||||
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
FROM RKVersion, RKMaster
|
||||||
|
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -746,9 +735,10 @@ class PhotosDB:
|
|||||||
RKVersion.momentUuid,
|
RKVersion.momentUuid,
|
||||||
RKVersion.rawMasterUuid,
|
RKVersion.rawMasterUuid,
|
||||||
RKVersion.nonRawMasterUuid,
|
RKVersion.nonRawMasterUuid,
|
||||||
RKMaster.alternateMasterUuid
|
RKMaster.alternateMasterUuid,
|
||||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
RKVersion.isInTrash
|
||||||
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
FROM RKVersion, RKMaster
|
||||||
|
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# order of results
|
# order of results
|
||||||
@@ -784,6 +774,7 @@ class PhotosDB:
|
|||||||
# 29 RKVersion.rawMasterUuid, -- UUID of RAW master
|
# 29 RKVersion.rawMasterUuid, -- UUID of RAW master
|
||||||
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
||||||
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
||||||
|
# 32 RKVersion.isInTrash
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -810,7 +801,7 @@ class PhotosDB:
|
|||||||
try:
|
try:
|
||||||
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(row[5] + td)
|
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(row[5] + td)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._dbphotos[uuid]["imageDate"] = datetime.date(1970, 1, 1)
|
self._dbphotos[uuid]["imageDate"] = datetime(1970, 1, 1)
|
||||||
|
|
||||||
self._dbphotos[uuid]["mainRating"] = row[6]
|
self._dbphotos[uuid]["mainRating"] = row[6]
|
||||||
self._dbphotos[uuid]["hasAdjustments"] = row[7]
|
self._dbphotos[uuid]["hasAdjustments"] = row[7]
|
||||||
@@ -926,6 +917,9 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
|
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
|
||||||
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
|
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
|
||||||
|
|
||||||
|
# recently deleted items
|
||||||
|
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||||
|
|
||||||
# get additional details from RKMaster, needed for RAW processing
|
# get additional details from RKMaster, needed for RAW processing
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
@@ -976,8 +970,7 @@ class PhotosDB:
|
|||||||
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
||||||
RKModelResource.attachedModelType, RKModelResource.resourceType
|
RKModelResource.attachedModelType, RKModelResource.resourceType
|
||||||
FROM RKVersion
|
FROM RKVersion
|
||||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId
|
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """
|
||||||
WHERE RKVersion.isInTrash = 0 """
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Order of results:
|
# Order of results:
|
||||||
@@ -1020,8 +1013,7 @@ class PhotosDB:
|
|||||||
RKAdjustmentData.originator,
|
RKAdjustmentData.originator,
|
||||||
RKAdjustmentData.format
|
RKAdjustmentData.format
|
||||||
FROM RKVersion, RKAdjustmentData
|
FROM RKVersion, RKAdjustmentData
|
||||||
WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid
|
WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid """
|
||||||
AND RKVersion.isInTrash = 0 """
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
@@ -1043,8 +1035,6 @@ class PhotosDB:
|
|||||||
INNER JOIN RKMaster on RKVersion.masterUuid = RKMaster.uuid
|
INNER JOIN RKMaster on RKVersion.masterUuid = RKMaster.uuid
|
||||||
INNER JOIN RKModelResource on RKMaster.modelId = RKModelResource.attachedModelId
|
INNER JOIN RKModelResource on RKMaster.modelId = RKModelResource.attachedModelId
|
||||||
WHERE RKModelResource.UTI = 'com.apple.quicktime-movie'
|
WHERE RKModelResource.UTI = 'com.apple.quicktime-movie'
|
||||||
AND RKMaster.isInTrash = 0
|
|
||||||
AND RKVersion.isInTrash = 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1289,7 +1279,6 @@ class PhotosDB:
|
|||||||
"SELECT ZPERSON.ZFULLNAME, ZGENERICASSET.ZUUID "
|
"SELECT ZPERSON.ZFULLNAME, ZGENERICASSET.ZUUID "
|
||||||
"FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET "
|
"FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET "
|
||||||
"WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK "
|
"WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK "
|
||||||
"AND ZGENERICASSET.ZTRASHEDSTATE = 0"
|
|
||||||
)
|
)
|
||||||
for person in c:
|
for person in c:
|
||||||
if person[0] is None:
|
if person[0] is None:
|
||||||
@@ -1313,7 +1302,6 @@ class PhotosDB:
|
|||||||
"FROM ZGENERICASSET "
|
"FROM ZGENERICASSET "
|
||||||
"JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK "
|
"JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK "
|
||||||
"JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS "
|
"JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS "
|
||||||
"WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 "
|
|
||||||
)
|
)
|
||||||
for album in c:
|
for album in c:
|
||||||
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
||||||
@@ -1415,7 +1403,6 @@ class PhotosDB:
|
|||||||
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
||||||
"JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK "
|
"JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK "
|
||||||
"JOIN ZKEYWORD ON ZKEYWORD.Z_PK = Z_1KEYWORDS.Z_37KEYWORDS "
|
"JOIN ZKEYWORD ON ZKEYWORD.Z_PK = Z_1KEYWORDS.Z_37KEYWORDS "
|
||||||
"WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 "
|
|
||||||
)
|
)
|
||||||
for keyword in c:
|
for keyword in c:
|
||||||
if not keyword[1] in self._dbkeywords_uuid:
|
if not keyword[1] in self._dbkeywords_uuid:
|
||||||
@@ -1469,10 +1456,10 @@ class PhotosDB:
|
|||||||
ZGENERICASSET.ZCLOUDASSETGUID,
|
ZGENERICASSET.ZCLOUDASSETGUID,
|
||||||
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
||||||
ZGENERICASSET.ZMOMENT,
|
ZGENERICASSET.ZMOMENT,
|
||||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||||
|
ZGENERICASSET.ZTRASHEDSTATE
|
||||||
FROM ZGENERICASSET
|
FROM ZGENERICASSET
|
||||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||||
WHERE ZGENERICASSET.ZTRASHEDSTATE = 0
|
|
||||||
ORDER BY ZGENERICASSET.ZUUID """
|
ORDER BY ZGENERICASSET.ZUUID """
|
||||||
)
|
)
|
||||||
# Order of results
|
# Order of results
|
||||||
@@ -1505,6 +1492,7 @@ class PhotosDB:
|
|||||||
# 25 ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA -- reverse geolocation data
|
# 25 ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA -- reverse geolocation data
|
||||||
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
||||||
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
||||||
|
# 28 ZGENERICASSET.ZTRASHEDSTATE -- 0 if not in trash, 1 if in trash
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -1528,7 +1516,7 @@ class PhotosDB:
|
|||||||
try:
|
try:
|
||||||
info["imageDate"] = datetime.fromtimestamp(row[5] + td)
|
info["imageDate"] = datetime.fromtimestamp(row[5] + td)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
info["imageDate"] = datetime.date(1970, 1, 1)
|
info["imageDate"] = datetime(1970, 1, 1)
|
||||||
|
|
||||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||||
info["hidden"] = row[9]
|
info["hidden"] = row[9]
|
||||||
@@ -1653,6 +1641,9 @@ class PhotosDB:
|
|||||||
info["original_resource_choice"] = row[27]
|
info["original_resource_choice"] = row[27]
|
||||||
info["raw_is_original"] = True if row[27] == 1 else False
|
info["raw_is_original"] = True if row[27] == 1 else False
|
||||||
|
|
||||||
|
# recently deleted items
|
||||||
|
info["intrash"] = True if row[28] == 1 else False
|
||||||
|
|
||||||
# associated RAW image info
|
# associated RAW image info
|
||||||
# will be filled in later
|
# will be filled in later
|
||||||
info["has_raw"] = False
|
info["has_raw"] = False
|
||||||
@@ -1724,8 +1715,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 +1723,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 +1856,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 +2079,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,
|
||||||
@@ -2100,6 +2148,7 @@ class PhotosDB:
|
|||||||
movies=False,
|
movies=False,
|
||||||
from_date=None,
|
from_date=None,
|
||||||
to_date=None,
|
to_date=None,
|
||||||
|
intrash=False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Return a list of PhotoInfo objects
|
Return a list of PhotoInfo objects
|
||||||
@@ -2116,6 +2165,8 @@ class PhotosDB:
|
|||||||
movies: if True, returns movie files, if False, does not return movies; default is False
|
movies: if True, returns movie files, if False, does not return movies; default is False
|
||||||
from_date: return photos with creation date >= from_date (datetime.datetime object, default None)
|
from_date: return photos with creation date >= from_date (datetime.datetime object, default None)
|
||||||
to_date: return photos with creation date <= to_date (datetime.datetime object, default None)
|
to_date: return photos with creation date <= to_date (datetime.datetime object, default None)
|
||||||
|
intrash: if True, returns only images in "Recently deleted items" folder,
|
||||||
|
if False returns only photos that aren't deleted; default is False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# implementation is a bit kludgy but it works
|
# implementation is a bit kludgy but it works
|
||||||
@@ -2123,6 +2174,15 @@ class PhotosDB:
|
|||||||
# use results to build a list of PhotoInfo objects
|
# use results to build a list of PhotoInfo objects
|
||||||
|
|
||||||
photos_sets = [] # list of photo sets to perform intersection of
|
photos_sets = [] # list of photo sets to perform intersection of
|
||||||
|
if intrash:
|
||||||
|
photos_sets.append(
|
||||||
|
{p for p in self._dbphotos if self._dbphotos[p]["intrash"]}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
photos_sets.append(
|
||||||
|
{p for p in self._dbphotos if not self._dbphotos[p]["intrash"]}
|
||||||
|
)
|
||||||
|
|
||||||
if not any([keywords, uuid, persons, albums, from_date, to_date]):
|
if not any([keywords, uuid, persons, albums, from_date, to_date]):
|
||||||
# return all the photos, filtering for images and movies
|
# return all the photos, filtering for images and movies
|
||||||
# append keys of all photos as a single set to photos_sets
|
# append keys of all photos as a single set to photos_sets
|
||||||
@@ -2131,12 +2191,15 @@ class PhotosDB:
|
|||||||
if albums:
|
if albums:
|
||||||
album_set = set()
|
album_set = set()
|
||||||
for album in albums:
|
for album in albums:
|
||||||
# TODO: can have >1 album with same name. This globs them together.
|
# glob together albums with same name
|
||||||
# Need a way to select which album?
|
|
||||||
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")
|
||||||
@@ -2169,6 +2232,7 @@ class PhotosDB:
|
|||||||
logging.debug(f"Could not find person '{person}' in database")
|
logging.debug(f"Could not find person '{person}' in database")
|
||||||
photos_sets.append(person_set)
|
photos_sets.append(person_set)
|
||||||
|
|
||||||
|
# sourcery off
|
||||||
if from_date or to_date:
|
if from_date or to_date:
|
||||||
dsel = self._dbphotos
|
dsel = self._dbphotos
|
||||||
if from_date:
|
if from_date:
|
||||||
@@ -2186,7 +2250,6 @@ class PhotosDB:
|
|||||||
photoinfo = []
|
photoinfo = []
|
||||||
if photos_sets: # found some photos
|
if photos_sets: # found some photos
|
||||||
# get the intersection of each argument/search criteria
|
# get the intersection of each argument/search criteria
|
||||||
logging.debug(f"Got photo_sets: {photos_sets}")
|
|
||||||
for p in set.intersection(*photos_sets):
|
for p in set.intersection(*photos_sets):
|
||||||
# filter for non-selected burst photos
|
# filter for non-selected burst photos
|
||||||
if self._dbphotos[p]["burst"] and not self._dbphotos[p]["burst_key"]:
|
if self._dbphotos[p]["burst"] and not self._dbphotos[p]["burst_key"]:
|
||||||
@@ -2215,5 +2278,7 @@ class PhotosDB:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
""" returns number of photos in the database """
|
""" Returns number of photos in the database
|
||||||
|
Includes recently deleted photos and non-selected burst images
|
||||||
|
"""
|
||||||
return len(self._dbphotos)
|
return len(self._dbphotos)
|
||||||
|
|||||||
@@ -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,7 @@
|
|||||||
# 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 datetime
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -35,6 +36,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 +52,31 @@ 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.",
|
||||||
|
"{today.date}": "Current date in iso format, e.g. '2020-03-22'",
|
||||||
|
"{today.year}": "4-digit year of current date",
|
||||||
|
"{today.yy}": "2-digit year of current date",
|
||||||
|
"{today.mm}": "2-digit month of the current date (zero padded)",
|
||||||
|
"{today.month}": "Month name in user's locale of the current date",
|
||||||
|
"{today.mon}": "Month abbreviation in the user's locale of the current date",
|
||||||
|
"{today.dd}": "2-digit day of the month (zero padded) of current date",
|
||||||
|
"{today.dow}": "Day of week in user's locale of the current date",
|
||||||
|
"{today.doy}": "3-digit day of year (e.g Julian day) of current date, starting from 1 (zero padded)",
|
||||||
|
"{today.hour}": "2-digit hour of the current date",
|
||||||
|
"{today.min}": "2-digit minute of the current date",
|
||||||
|
"{today.sec}": "2-digit second of the current date",
|
||||||
|
"{today.strftime}": "Apply strftime template to current date/time. Should be used in form "
|
||||||
|
+ "{today.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
|
+ "{today.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 +105,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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -86,11 +120,22 @@ class PhotoTemplate:
|
|||||||
"""
|
"""
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
|
|
||||||
|
# holds value of current date/time for {today.x} fields
|
||||||
|
# gets initialized in get_template_value
|
||||||
|
self.today = None
|
||||||
|
|
||||||
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 +152,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 +167,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 +217,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 +228,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 +266,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).
|
||||||
@@ -233,173 +280,278 @@ class PhotoTemplate:
|
|||||||
ValueError if no rule exists for field.
|
ValueError if no rule exists for field.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# initialize today with current date/time if needed
|
||||||
|
if self.today is None:
|
||||||
|
self.today = datetime.datetime.now()
|
||||||
|
|
||||||
# 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 == "today.date":
|
||||||
|
return DateTimeFormatter(self.today).date
|
||||||
|
|
||||||
|
if field == "today.year":
|
||||||
|
return DateTimeFormatter(self.today).year
|
||||||
|
|
||||||
|
if field == "today.yy":
|
||||||
|
return DateTimeFormatter(self.today).yy
|
||||||
|
|
||||||
|
if field == "today.mm":
|
||||||
|
return DateTimeFormatter(self.today).mm
|
||||||
|
|
||||||
|
if field == "today.month":
|
||||||
|
return DateTimeFormatter(self.today).month
|
||||||
|
|
||||||
|
if field == "today.mon":
|
||||||
|
return DateTimeFormatter(self.today).mon
|
||||||
|
|
||||||
|
if field == "today.dd":
|
||||||
|
return DateTimeFormatter(self.today).dd
|
||||||
|
|
||||||
|
if field == "today.dow":
|
||||||
|
return DateTimeFormatter(self.today).dow
|
||||||
|
|
||||||
|
if field == "today.doy":
|
||||||
|
return DateTimeFormatter(self.today).doy
|
||||||
|
|
||||||
|
if field == "today.hour":
|
||||||
|
return DateTimeFormatter(self.today).hour
|
||||||
|
|
||||||
|
if field == "today.min":
|
||||||
|
return DateTimeFormatter(self.today).min
|
||||||
|
|
||||||
|
if field == "today.sec":
|
||||||
|
return DateTimeFormatter(self.today).sec
|
||||||
|
|
||||||
|
if field == "today.strftime":
|
||||||
|
if default:
|
||||||
|
try:
|
||||||
|
return self.today.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,23 @@ 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 = PostalAddress(
|
||||||
|
None, None, None, None, None, None, None, 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 +629,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_):
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -5,6 +5,6 @@
|
|||||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||||
<date>2020-04-25T23:54:43Z</date>
|
<date>2020-04-25T23:54:43Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||||
<date>2020-04-26T06:26:10Z</date>
|
<date>2020-06-27T16:03:48Z</date>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||||
<date>2020-04-25T23:54:29Z</date>
|
<date>2020-06-27T16:03:43Z</date>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>LastHistoryRowId</key>
|
<key>LastHistoryRowId</key>
|
||||||
<integer>606</integer>
|
<integer>651</integer>
|
||||||
<key>LibraryBuildTag</key>
|
<key>LibraryBuildTag</key>
|
||||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||||
<key>LibrarySchemaVersion</key>
|
<key>LibrarySchemaVersion</key>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -24,7 +24,7 @@
|
|||||||
<key>SnapshotCompletedDate</key>
|
<key>SnapshotCompletedDate</key>
|
||||||
<date>2019-07-27T13:16:43Z</date>
|
<date>2019-07-27T13:16:43Z</date>
|
||||||
<key>SnapshotLastValidated</key>
|
<key>SnapshotLastValidated</key>
|
||||||
<date>2020-04-25T23:56:35Z</date>
|
<date>2020-06-27T16:03:33Z</date>
|
||||||
<key>SnapshotTables</key>
|
<key>SnapshotTables</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -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>763</integer>
|
||||||
<key>processname</key>
|
<key>processname</key>
|
||||||
<string>photolibraryd</string>
|
<string>photolibraryd</string>
|
||||||
<key>uid</key>
|
<key>uid</key>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -10,6 +10,7 @@
|
|||||||
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
|
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
|
||||||
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
|
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
|
||||||
<string>2C2AF115-BD1D-4434-A747-D1C8BD8E2045/L0/020</string>
|
<string>2C2AF115-BD1D-4434-A747-D1C8BD8E2045/L0/020</string>
|
||||||
|
<string>CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020</string>
|
||||||
</array>
|
</array>
|
||||||
<key>Photos</key>
|
<key>Photos</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -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-24T04:02:13Z</date>
|
||||||
<key>BackgroundHighlightEnrichment</key>
|
<key>BackgroundHighlightEnrichment</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-24T04:02:12Z</date>
|
||||||
<key>BackgroundJobAssetRevGeocode</key>
|
<key>BackgroundJobAssetRevGeocode</key>
|
||||||
<date>2020-05-30T04:01:24Z</date>
|
<date>2020-06-24T04:02:13Z</date>
|
||||||
<key>BackgroundJobSearch</key>
|
<key>BackgroundJobSearch</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-24T04:02:13Z</date>
|
||||||
<key>BackgroundPeopleSuggestion</key>
|
<key>BackgroundPeopleSuggestion</key>
|
||||||
<date>2020-05-30T01:45:51Z</date>
|
<date>2020-06-24T04:02:12Z</date>
|
||||||
<key>BackgroundUserBehaviorProcessor</key>
|
<key>BackgroundUserBehaviorProcessor</key>
|
||||||
<date>2020-05-29T04:31:38Z</date>
|
<date>2020-06-24T04:02:13Z</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-24T04:02:13Z</date>
|
||||||
<key>SiriPortraitDonation</key>
|
<key>SiriPortraitDonation</key>
|
||||||
<date>2020-05-29T04:31:38Z</date>
|
<date>2020-06-24T04:02:13Z</date>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 87 KiB |
BIN
tests/test-images/IMG_1693.tif
Normal file
@@ -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,10 @@ 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 = 14
|
||||||
|
PHOTOS_NOT_IN_TRASH_LEN = 13
|
||||||
|
PHOTOS_IN_TRASH_LEN = 1
|
||||||
|
|
||||||
KEYWORDS = [
|
KEYWORDS = [
|
||||||
"Kids",
|
"Kids",
|
||||||
"wedding",
|
"wedding",
|
||||||
@@ -22,10 +26,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 +49,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 +66,19 @@ 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"
|
||||||
|
"date_invalid": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||||
|
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||||
|
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 +128,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 +145,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 +233,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 +248,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 +397,63 @@ 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_NOT_IN_TRASH_LEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_1():
|
||||||
|
""" test PhotosDB.photos(intrash=True) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=True)
|
||||||
|
assert len(photos) == PHOTOS_IN_TRASH_LEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_2():
|
||||||
|
""" test PhotosDB.photos(intrash=True) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=True)
|
||||||
|
for p in photos:
|
||||||
|
assert p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_2():
|
||||||
|
""" test PhotosDB.photos(intrash=False) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=False)
|
||||||
|
for p in photos:
|
||||||
|
assert not p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_intrash_1():
|
||||||
|
""" Test PhotoInfo.intrash """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||||
|
assert p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_intrash_2():
|
||||||
|
""" Test PhotoInfo.intrash and intrash=default"""
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
|
||||||
|
assert not p
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_not_intrash():
|
||||||
|
""" Test PhotoInfo.intrash """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||||
|
assert not p.intrash
|
||||||
|
|
||||||
|
|
||||||
def test_keyword_2():
|
def test_keyword_2():
|
||||||
@@ -402,6 +477,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 +503,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 +835,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,12 +929,39 @@ 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) == 6
|
||||||
|
|
||||||
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) == 7
|
||||||
|
|
||||||
photos = photosdb.photos(
|
photos = photosdb.photos(
|
||||||
from_date=dt.datetime(2018, 9, 28), to_date=dt.datetime(2018, 9, 29)
|
from_date=dt.datetime(2018, 9, 28), to_date=dt.datetime(2018, 9, 29)
|
||||||
)
|
)
|
||||||
assert len(photos) == 4
|
assert len(photos) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_invalid():
|
||||||
|
""" Test date is invalid """
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
delta = timedelta(seconds=p.tzoffset)
|
||||||
|
tz = timezone(delta)
|
||||||
|
assert p.date == datetime(1970, 1, 1).astimezone(tz=tz)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_modified_invalid():
|
||||||
|
""" Test date modified is invalid """
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.date_modified is None
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -41,8 +41,15 @@ ALBUM_DICT = {
|
|||||||
UUID_DICT = {
|
UUID_DICT = {
|
||||||
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||||
"not_favorite": "8SOE9s0XQVGsuq4ONohTng",
|
"not_favorite": "8SOE9s0XQVGsuq4ONohTng",
|
||||||
|
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
|
||||||
|
"intrash": "3tljdX43R8+k6peNHVrJNQ",
|
||||||
|
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PHOTOS_DB_LEN = 8
|
||||||
|
PHOTOS_NOT_IN_TRASH_LEN = 7
|
||||||
|
PHOTOS_IN_TRASH_LEN = 1
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -58,12 +65,13 @@ def test_db_version():
|
|||||||
assert photosdb.db_version in osxphotos._constants._TESTED_DB_VERSIONS
|
assert photosdb.db_version in osxphotos._constants._TESTED_DB_VERSIONS
|
||||||
assert photosdb.db_version == "4025"
|
assert photosdb.db_version == "4025"
|
||||||
|
|
||||||
|
|
||||||
def test_db_len():
|
def test_db_len():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
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) == 7
|
assert len(photosdb) == PHOTOS_DB_LEN
|
||||||
|
|
||||||
|
|
||||||
def test_os_version():
|
def test_os_version():
|
||||||
@@ -307,7 +315,63 @@ def test_count():
|
|||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photos = photosdb.photos()
|
photos = photosdb.photos()
|
||||||
assert len(photos) == 7
|
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_1():
|
||||||
|
""" test PhotosDB.photos(intrash=True) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=True)
|
||||||
|
assert len(photos) == PHOTOS_IN_TRASH_LEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_2():
|
||||||
|
""" test PhotosDB.photos(intrash=True) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=True)
|
||||||
|
for p in photos:
|
||||||
|
assert p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photos_intrash_2():
|
||||||
|
""" test PhotosDB.photos(intrash=False) """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(intrash=False)
|
||||||
|
for p in photos:
|
||||||
|
assert not p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_intrash_1():
|
||||||
|
""" Test PhotoInfo.intrash """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||||
|
assert p.intrash
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_intrash_2():
|
||||||
|
""" Test PhotoInfo.intrash and intrash=default"""
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
|
||||||
|
assert not p
|
||||||
|
|
||||||
|
|
||||||
|
def test_photoinfo_not_intrash():
|
||||||
|
""" Test PhotoInfo.intrash """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||||
|
assert not p.intrash
|
||||||
|
|
||||||
|
|
||||||
def test_keyword_2():
|
def test_keyword_2():
|
||||||
@@ -407,3 +471,30 @@ def test_multi_person():
|
|||||||
photos = photosdb.photos(persons=["Katie", "Suzy"])
|
photos = photosdb.photos(persons=["Katie", "Suzy"])
|
||||||
|
|
||||||
assert len(photos) == 3
|
assert len(photos) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_invalid():
|
||||||
|
""" Test date is invalid """
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
delta = timedelta(seconds=p.tzoffset)
|
||||||
|
tz = timezone(delta)
|
||||||
|
assert p.date == datetime(1970, 1, 1).astimezone(tz=tz)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_modified_invalid():
|
||||||
|
""" Test date modified is invalid """
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.date_modified is None
|
||||||
|
|
||||||
|
|||||||
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] == "_"
|
||||||
|
|||||||
70
tests/test_template_today.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PHOTOS_DB_PLACES = (
|
||||||
|
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
DATETIME_TODAY = datetime.datetime(2020, 6, 21, 13, 0, 0)
|
||||||
|
""" Used to patch osxphotos.phototemplate.TODAY for testing """
|
||||||
|
|
||||||
|
UUID_DICT = {
|
||||||
|
"place_dc": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||||
|
"1_1_2": "1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||||
|
"2_1_1": "D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||||
|
"0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
|
||||||
|
"folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||||
|
"folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||||
|
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
|
||||||
|
}
|
||||||
|
|
||||||
|
TODAY_VALUES = {
|
||||||
|
"{today.date}": "2020-06-21",
|
||||||
|
"{today.year}": "2020",
|
||||||
|
"{today.yy}": "20",
|
||||||
|
"{today.mm}": "06",
|
||||||
|
"{today.month}": "June",
|
||||||
|
"{today.mon}": "Jun",
|
||||||
|
"{today.dd}": "21",
|
||||||
|
"{today.dow}": "Sunday",
|
||||||
|
"{today.doy}": "173",
|
||||||
|
"{today.hour}": "13",
|
||||||
|
"{today.min}": "00",
|
||||||
|
"{today.sec}": "00",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_today():
|
||||||
|
""" Test that substitutions are correct for {today.x}"""
|
||||||
|
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]
|
||||||
|
|
||||||
|
photo_template = osxphotos.PhotoTemplate(photo)
|
||||||
|
photo_template.today = DATETIME_TODAY
|
||||||
|
|
||||||
|
for template in TODAY_VALUES:
|
||||||
|
rendered, _ = photo_template.render(template)
|
||||||
|
assert rendered[0] == TODAY_VALUES[template]
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_strftime_today():
|
||||||
|
""" Test that strftime substitutions are correct for {today.strftime}"""
|
||||||
|
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]
|
||||||
|
|
||||||
|
photo_template = osxphotos.PhotoTemplate(photo)
|
||||||
|
photo_template.today = DATETIME_TODAY
|
||||||
|
rendered, unmatched = photo_template.render("{today.strftime,%Y-%m-%d-%H%M%S}")
|
||||||
|
assert rendered[0] == "2020-06-21-130000"
|
||||||
|
|
||||||
|
rendered, unmatched = photo.render_template("{today.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,
|
||||||
)
|
)
|
||||||
|
|||||||