Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfabd0dbea | ||
|
|
a23259948c | ||
|
|
1212fad4ad | ||
|
|
567abe3311 | ||
|
|
5a832181f7 | ||
|
|
4da57a1cee | ||
|
|
1fd0f96b14 | ||
|
|
e98c3fe429 | ||
|
|
d77e9747cd | ||
|
|
43d28e78f3 | ||
|
|
00bc50490e | ||
|
|
f8743c33bd | ||
|
|
937da9e617 | ||
|
|
435868a0a7 | ||
|
|
d9802247d9 | ||
|
|
f39a92a352 | ||
|
|
40dc7d32f2 | ||
|
|
4cd6c8f617 | ||
|
|
0004250e74 | ||
|
|
868ee7737b | ||
|
|
5387f8e2f9 | ||
|
|
73b499f405 |
116
CHANGELOG.md
116
CHANGELOG.md
@@ -4,20 +4,76 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.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)
|
||||
- Partial fix for #155 [`2271d89`](https://github.com/RhetTbull/osxphotos/commit/2271d8935507ecc27e6227b11b4796f2f4d2f10d)
|
||||
- Partial fix for #155 [`62d096b`](https://github.com/RhetTbull/osxphotos/commit/62d096b5a1a7e960195ec5c48fc9cffbebf2c735)
|
||||
|
||||
#### [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)
|
||||
- Updated CHANGELOG.md [`d47fd46`](https://github.com/RhetTbull/osxphotos/commit/d47fd46a21881bea86d1bc624c6027e2cbe08d9c)
|
||||
|
||||
#### [v0.29.8](https://github.com/RhetTbull/osxphotos/compare/v0.29.5...v0.29.8)
|
||||
|
||||
@@ -39,7 +95,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 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)
|
||||
- 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)
|
||||
|
||||
#### [v0.29.2](https://github.com/RhetTbull/osxphotos/compare/v0.29.1...v0.29.2)
|
||||
@@ -53,7 +109,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 23 May 2020
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -63,8 +119,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 as_dict to PlaceInfo [`8c4fe40`](https://github.com/RhetTbull/osxphotos/commit/8c4fe40aa6850f166e526cffaa088550884399af)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -73,16 +129,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)
|
||||
- Revert "test library updates" [`48e9c32`](https://github.com/RhetTbull/osxphotos/commit/48e9c32add549e66c3ef8c65f8821f5033b55b11)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
> 14 May 2020
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -99,8 +154,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 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)
|
||||
- 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 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)
|
||||
- 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)
|
||||
|
||||
@@ -124,7 +179,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 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)
|
||||
- Updated CHANGELOG.md [`072a8d7`](https://github.com/RhetTbull/osxphotos/commit/072a8d795e5e15fa8ca8d8872aecf4cddd7837f7)
|
||||
- Update README.md [`5cc98c3`](https://github.com/RhetTbull/osxphotos/commit/5cc98c338bcc19fd05bf293eb3afe24c07c8b380)
|
||||
- Updated README.md [`a800711`](https://github.com/RhetTbull/osxphotos/commit/a80071111f810a1d7d6e2d735839e85499091ea4)
|
||||
- Update README.md [`1c9d4f2`](https://github.com/RhetTbull/osxphotos/commit/1c9d4f282beea2ac12273c8d0f9453bad1255c2c)
|
||||
@@ -135,7 +189,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)
|
||||
- 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)
|
||||
|
||||
#### [v0.28.6](https://github.com/RhetTbull/osxphotos/compare/v0.28.5...v0.28.6)
|
||||
@@ -143,7 +196,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 26 April 2020
|
||||
|
||||
- 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)
|
||||
- Update pythonpackage.yml to remove older pythons [`ccb5f25`](https://github.com/RhetTbull/osxphotos/commit/ccb5f252d14e9335ae04a2e338a6d527b80c9a93)
|
||||
|
||||
@@ -157,7 +210,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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -165,7 +218,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)
|
||||
- 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)
|
||||
- test library update [`3bac106`](https://github.com/RhetTbull/osxphotos/commit/3bac106eb7a180e9e39643a89087d92bf2a437d0)
|
||||
|
||||
@@ -183,7 +235,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)
|
||||
- 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)
|
||||
|
||||
@@ -197,7 +248,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 12 April 2020
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -206,8 +256,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- 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 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 [`1aa3838`](https://github.com/RhetTbull/osxphotos/commit/1aa3838c3866a18084ffe822de02df0eda464d71)
|
||||
|
||||
#### [v0.26.1](https://github.com/RhetTbull/osxphotos/compare/v0.26.0...v0.26.1)
|
||||
|
||||
@@ -215,7 +265,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)
|
||||
- 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)
|
||||
|
||||
@@ -223,7 +272,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)
|
||||
- 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)
|
||||
|
||||
@@ -242,7 +291,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 places command to CLI [`fd5e748`](https://github.com/RhetTbull/osxphotos/commit/fd5e748dca759ea1c3a7329d447f363afe8418b7)
|
||||
- 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)
|
||||
|
||||
@@ -263,8 +312,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 22 March 2020
|
||||
|
||||
- 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 example [`8f0307f`](https://github.com/RhetTbull/osxphotos/commit/8f0307fc24345ca0e87017ac76791c9bbe8db25e)
|
||||
|
||||
#### [v0.23.3](https://github.com/RhetTbull/osxphotos/compare/v0.23.1...v0.23.3)
|
||||
|
||||
@@ -279,15 +328,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 21 March 2020
|
||||
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
> 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)
|
||||
- Updated CHANGELOG.md [`816b98e`](https://github.com/RhetTbull/osxphotos/commit/816b98e617c30d0bdb51bc2413f9915742c8592e)
|
||||
- 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)
|
||||
@@ -303,7 +352,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)
|
||||
- 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)
|
||||
|
||||
@@ -318,18 +367,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)
|
||||
- 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)
|
||||
- README.md update [`a27ce33`](https://github.com/RhetTbull/osxphotos/commit/a27ce33473df3260dfb7ed26e28295cbf87d1e78)
|
||||
- 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)
|
||||
|
||||
> 8 March 2020
|
||||
|
||||
- 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)
|
||||
|
||||
#### [v0.22.12](https://github.com/RhetTbull/osxphotos/compare/0.22.10...v0.22.12)
|
||||
@@ -346,7 +394,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)
|
||||
- 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)
|
||||
|
||||
@@ -358,7 +406,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -372,7 +420,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 [`4d36b3b`](https://github.com/RhetTbull/osxphotos/commit/4d36b3b31f3e0e74d9d111b6b691771e19f94086)
|
||||
- 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)
|
||||
|
||||
@@ -384,7 +432,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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -392,7 +440,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)
|
||||
- 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)
|
||||
|
||||
@@ -407,8 +455,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 4 January 2020
|
||||
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -422,9 +470,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
> 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 [`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)
|
||||
|
||||
|
||||
110
README.md
110
README.md
@@ -17,6 +17,7 @@
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
+ [Template Substitutions](#template-substitutions)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
* [Examples](#examples)
|
||||
@@ -34,7 +35,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
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:
|
||||
`pipx install osxphotos`
|
||||
@@ -90,6 +91,7 @@ Commands:
|
||||
help Print help; for help on commands: help <command>.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
@@ -125,13 +127,13 @@ Options:
|
||||
-V, --verbose Print verbose output.
|
||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||
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
|
||||
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
|
||||
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. If more than one folder, treated as
|
||||
"OR", e.g. find photos in any FOLDER. Only
|
||||
@@ -146,11 +148,15 @@ Options:
|
||||
geolocation info
|
||||
--no-place Search for photos with no associated place
|
||||
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
|
||||
identifier (UTI) matches UTI
|
||||
-i, --ignore-case Case insensitive search for title,
|
||||
description, or place. Does not apply to
|
||||
keyword, person, or album.
|
||||
description, place, keyword, person, or
|
||||
album.
|
||||
--edited Search for photos that have been edited.
|
||||
--external-edit Search for photos edited in external editor.
|
||||
--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
|
||||
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
|
||||
{name} Current filename of the photo
|
||||
@@ -391,6 +398,18 @@ Substitution Description
|
||||
creation time
|
||||
{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.year} 4-digit year of file modification time
|
||||
@@ -406,6 +425,9 @@ Substitution Description
|
||||
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
||||
modification time, starting from 1 (zero
|
||||
padded)
|
||||
{modified.hour} 2-digit hour of the file modification time
|
||||
{modified.min} 2-digit minute of the file modification time
|
||||
{modified.sec} 2-digit second of the file modification time
|
||||
{place.name} Place name from the photo's reverse
|
||||
geolocation data, as displayed in Photos
|
||||
{place.country_code} The ISO country code from the photo's
|
||||
@@ -807,6 +829,23 @@ photosdb.db_version
|
||||
|
||||
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
|
||||
|
||||
#### `get_db_connection()`
|
||||
Returns tuple of (connection, cursor) for the working copy of the Photos database. This is useful for debugging or prototyping new features.
|
||||
|
||||
```python
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
conn, cursor = photosdb.get_db_connection()
|
||||
|
||||
results = conn.execute(
|
||||
"SELECT ZUUID FROM ZGENERICASSET WHERE ZFAVORITE = 1;"
|
||||
).fetchall()
|
||||
|
||||
for row in results:
|
||||
# do something
|
||||
pass
|
||||
|
||||
conn.close()
|
||||
```
|
||||
|
||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
|
||||
|
||||
@@ -1128,7 +1167,12 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo")
|
||||
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()`
|
||||
Returns a JSON representation of all photo info
|
||||
@@ -1374,6 +1418,45 @@ PostalAddress(street='3700 Wailea Alanui Dr', sub_locality=None, city='Kihei', s
|
||||
>>> photo.place.address.postal_code
|
||||
'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
|
||||
|
||||
@@ -1392,7 +1475,12 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
||||
|{created.month}|Month name in user's locale of the file creation time|
|
||||
|{created.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.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.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.year}|4-digit year of file modification time|
|
||||
|{modified.yy}|2-digit year of file modification time|
|
||||
@@ -1401,6 +1489,9 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
||||
|{modified.mon}|Month abbreviation in the user's locale of the file modification time|
|
||||
|{modified.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.hour}|2-digit hour of the file modification time|
|
||||
|{modified.min}|2-digit minute of the file modification time|
|
||||
|{modified.sec}|2-digit second of the file modification time|
|
||||
|{place.name}|Place name from the photo's reverse geolocation data, as displayed in Photos|
|
||||
|{place.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|
|
||||
@@ -1421,7 +1512,6 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|
||||
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
||||
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|
||||
|
||||
|
||||
### Utility Functions
|
||||
|
||||
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:
|
||||
# python3 -i examples/photos_repl.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
@@ -17,6 +18,23 @@ import osxphotos
|
||||
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():
|
||||
db = None
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ def query_options(f):
|
||||
default=None,
|
||||
multiple=True,
|
||||
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(
|
||||
"--person",
|
||||
@@ -253,7 +253,7 @@ def query_options(f):
|
||||
default=None,
|
||||
multiple=True,
|
||||
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(
|
||||
"--album",
|
||||
@@ -261,7 +261,7 @@ def query_options(f):
|
||||
default=None,
|
||||
multiple=True,
|
||||
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(
|
||||
"--folder",
|
||||
@@ -311,6 +311,13 @@ def query_options(f):
|
||||
is_flag=True,
|
||||
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(
|
||||
"--uti",
|
||||
metavar="UTI",
|
||||
@@ -322,7 +329,7 @@ def query_options(f):
|
||||
"-i",
|
||||
"--ignore-case",
|
||||
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(
|
||||
@@ -527,7 +534,9 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
|
||||
def keywords(ctx, cli_obj, db, json_, 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:
|
||||
click.echo(cli.commands["keywords"].get_help(ctx), err=True)
|
||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||
@@ -551,7 +560,9 @@ def keywords(ctx, cli_obj, db, json_, photos_library):
|
||||
def albums(ctx, cli_obj, db, json_, 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:
|
||||
click.echo(cli.commands["albums"].get_help(ctx), err=True)
|
||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||
@@ -578,7 +589,9 @@ def albums(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. """
|
||||
|
||||
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:
|
||||
click.echo(cli.commands["persons"].get_help(ctx), err=True)
|
||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||
@@ -593,6 +606,32 @@ def persons(ctx, cli_obj, db, json_, photos_library):
|
||||
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()
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@@ -861,6 +900,7 @@ def query(
|
||||
has_raw,
|
||||
place,
|
||||
no_place,
|
||||
label,
|
||||
):
|
||||
""" Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
@@ -881,6 +921,7 @@ def query(
|
||||
has_raw,
|
||||
from_date,
|
||||
to_date,
|
||||
label,
|
||||
]
|
||||
exclusive = [
|
||||
(favorite, not_favorite),
|
||||
@@ -976,6 +1017,7 @@ def query(
|
||||
has_raw=has_raw,
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
label=label,
|
||||
)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
@@ -1209,6 +1251,7 @@ def export(
|
||||
place,
|
||||
no_place,
|
||||
no_extended_attributes,
|
||||
label,
|
||||
):
|
||||
""" Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
@@ -1287,13 +1330,32 @@ def export(
|
||||
return
|
||||
|
||||
# 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:
|
||||
export_db = ExportDBInMemory(os.path.join(dest, OSXPHOTOS_EXPORT_DB))
|
||||
export_db = ExportDBInMemory(export_db_path)
|
||||
# echo = functools.partial(click.echo, err=True)
|
||||
# fileutil = FileUtilNoOp(verbose=echo)
|
||||
fileutil = FileUtilNoOp
|
||||
else:
|
||||
export_db = ExportDB(os.path.join(dest, OSXPHOTOS_EXPORT_DB))
|
||||
export_db = ExportDB(export_db_path)
|
||||
fileutil = FileUtil
|
||||
|
||||
photos = _query(
|
||||
@@ -1348,6 +1410,7 @@ def export(
|
||||
has_raw=has_raw,
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
label=label,
|
||||
)
|
||||
|
||||
results_exported = []
|
||||
@@ -1460,7 +1523,7 @@ def export(
|
||||
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} seconds")
|
||||
click.echo(f"Elapsed time: {(stop_time-start_time):.3f} seconds")
|
||||
else:
|
||||
click.echo("Did not find any photos to export")
|
||||
|
||||
@@ -1636,6 +1699,7 @@ def _query(
|
||||
has_raw=None,
|
||||
place=None,
|
||||
no_place=None,
|
||||
label=None,
|
||||
):
|
||||
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
||||
used by query and export commands
|
||||
@@ -1644,16 +1708,21 @@ def _query(
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
photos = photosdb.photos(
|
||||
keywords=keyword,
|
||||
persons=person,
|
||||
albums=album,
|
||||
uuid=uuid,
|
||||
images=isphoto,
|
||||
movies=ismovie,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
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:
|
||||
# search for photos in an album in folder
|
||||
# finds photos that have albums whose top level folder matches folder
|
||||
@@ -1828,6 +1897,34 @@ def _query(
|
||||
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(
|
||||
photo=None,
|
||||
dest=None,
|
||||
@@ -2108,7 +2205,7 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
dest_path = os.path.join(dest, dirname)
|
||||
if not is_valid_filepath(dest_path, platform="auto"):
|
||||
raise ValueError(f"Invalid file path: '{dest_path}'")
|
||||
if not (dry_run or os.path.isdir(dest_path)):
|
||||
if not dry_run and not os.path.isdir(dest_path):
|
||||
os.makedirs(dest_path)
|
||||
dest_paths.append(dest_path)
|
||||
else:
|
||||
@@ -2116,5 +2213,42 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
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__":
|
||||
cli() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.29.14"
|
||||
__version__ = "0.29.24"
|
||||
|
||||
@@ -6,4 +6,5 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
|
||||
from ._photoinfo_exifinfo import ExifInfo
|
||||
from ._photoinfo_export import ExportResults
|
||||
from ._photoinfo_scoreinfo import ScoreInfo
|
||||
from .photoinfo import PhotoInfo
|
||||
|
||||
119
osxphotos/photoinfo/_photoinfo_scoreinfo.py
Normal file
119
osxphotos/photoinfo/_photoinfo_scoreinfo.py
Normal file
@@ -0,0 +1,119 @@
|
||||
""" PhotoInfo methods to expose computed score info from the library """
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .._constants import _PHOTOS_4_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScoreInfo:
|
||||
""" Computed photo score info associated with a photo from the Photos library """
|
||||
|
||||
overall: float
|
||||
curation: float
|
||||
promotion: float
|
||||
highlight_visibility: float
|
||||
behavioral: float
|
||||
failure: float
|
||||
harmonious_color: float
|
||||
immersiveness: float
|
||||
interaction: float
|
||||
interesting_subject: float
|
||||
intrusive_object_presence: float
|
||||
lively_color: float
|
||||
low_light: float
|
||||
noise: float
|
||||
pleasant_camera_tilt: float
|
||||
pleasant_composition: float
|
||||
pleasant_lighting: float
|
||||
pleasant_pattern: float
|
||||
pleasant_perspective: float
|
||||
pleasant_post_processing: float
|
||||
pleasant_reflection: float
|
||||
pleasant_symmetry: float
|
||||
sharply_focused_subject: float
|
||||
tastefully_blurred: float
|
||||
well_chosen_subject: float
|
||||
well_framed_subject: float
|
||||
well_timed_shot: float
|
||||
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
""" Computed score information for a photo
|
||||
|
||||
Returns:
|
||||
ScoreInfo instance
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"score not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||
except AttributeError:
|
||||
try:
|
||||
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=scores["overall_aesthetic"],
|
||||
curation=scores["curation"],
|
||||
promotion=scores["promotion"],
|
||||
highlight_visibility=scores["highlight_visibility"],
|
||||
behavioral=scores["behavioral"],
|
||||
failure=scores["failure"],
|
||||
harmonious_color=scores["harmonious_color"],
|
||||
immersiveness=scores["immersiveness"],
|
||||
interaction=scores["interaction"],
|
||||
interesting_subject=scores["interesting_subject"],
|
||||
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||
lively_color=scores["lively_color"],
|
||||
low_light=scores["low_light"],
|
||||
noise=scores["noise"],
|
||||
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||
pleasant_composition=scores["pleasant_composition"],
|
||||
pleasant_lighting=scores["pleasant_lighting"],
|
||||
pleasant_pattern=scores["pleasant_pattern"],
|
||||
pleasant_perspective=scores["pleasant_perspective"],
|
||||
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||
pleasant_reflection=scores["pleasant_reflection"],
|
||||
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||
tastefully_blurred=scores["tastefully_blurred"],
|
||||
well_chosen_subject=scores["well_chosen_subject"],
|
||||
well_framed_subject=scores["well_framed_subject"],
|
||||
well_timed_shot=scores["well_timed_shot"],
|
||||
)
|
||||
return self._scoreinfo
|
||||
except KeyError:
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=0.0,
|
||||
curation=0.0,
|
||||
promotion=0.0,
|
||||
highlight_visibility=0.0,
|
||||
behavioral=0.0,
|
||||
failure=0.0,
|
||||
harmonious_color=0.0,
|
||||
immersiveness=0.0,
|
||||
interaction=0.0,
|
||||
interesting_subject=0.0,
|
||||
intrusive_object_presence=0.0,
|
||||
lively_color=0.0,
|
||||
low_light=0.0,
|
||||
noise=0.0,
|
||||
pleasant_camera_tilt=0.0,
|
||||
pleasant_composition=0.0,
|
||||
pleasant_lighting=0.0,
|
||||
pleasant_pattern=0.0,
|
||||
pleasant_perspective=0.0,
|
||||
pleasant_post_processing=0.0,
|
||||
pleasant_reflection=0.0,
|
||||
pleasant_symmetry=0.0,
|
||||
sharply_focused_subject=0.0,
|
||||
tastefully_blurred=0.0,
|
||||
well_chosen_subject=0.0,
|
||||
well_framed_subject=0.0,
|
||||
well_timed_shot=0.0,
|
||||
)
|
||||
return self._scoreinfo
|
||||
@@ -21,12 +21,15 @@ import yaml
|
||||
from .._constants import (
|
||||
_MOVIE_TYPE,
|
||||
_PHOTO_TYPE,
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
)
|
||||
from ..albuminfo import AlbumInfo
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..phototemplate import PhotoTemplate
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
|
||||
|
||||
|
||||
@@ -55,6 +58,7 @@ class PhotoInfo:
|
||||
_xmp_sidecar,
|
||||
ExportResults,
|
||||
)
|
||||
from ._photoinfo_scoreinfo import score, ScoreInfo
|
||||
|
||||
def __init__(self, db=None, uuid=None, info=None):
|
||||
self._uuid = uuid
|
||||
@@ -340,21 +344,40 @@ class PhotoInfo:
|
||||
@property
|
||||
def albums(self):
|
||||
""" list of albums picture is contained in """
|
||||
albums = []
|
||||
for album in self._info["albums"]:
|
||||
if not self._db._dbalbum_details[album]["intrash"]:
|
||||
albums.append(self._db._dbalbum_details[album]["title"])
|
||||
return albums
|
||||
try:
|
||||
return self._albums
|
||||
except AttributeError:
|
||||
self._albums = []
|
||||
album_kinds = (
|
||||
[_PHOTOS_4_ALBUM_KIND]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION
|
||||
else [_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_SHARED_ALBUM_KIND]
|
||||
)
|
||||
|
||||
for album in self._info["albums"]:
|
||||
detail = self._db._dbalbum_details[album]
|
||||
if detail["kind"] in album_kinds and not detail["intrash"]:
|
||||
self._albums.append(detail["title"])
|
||||
return self._albums
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" list of AlbumInfo objects representing albums the photos is contained in """
|
||||
albums = []
|
||||
for album in self._info["albums"]:
|
||||
if not self._db._dbalbum_details[album]["intrash"]:
|
||||
albums.append(AlbumInfo(db=self._db, uuid=album))
|
||||
try:
|
||||
return self._album_info
|
||||
except AttributeError:
|
||||
self._album_info = []
|
||||
album_kinds = (
|
||||
[_PHOTOS_4_ALBUM_KIND]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION
|
||||
else [_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_SHARED_ALBUM_KIND]
|
||||
)
|
||||
|
||||
return albums
|
||||
for album in self._info["albums"]:
|
||||
detail = self._db._dbalbum_details[album]
|
||||
if detail["kind"] in album_kinds and not detail["intrash"]:
|
||||
self._album_info.append(AlbumInfo(db=self._db, uuid=album))
|
||||
return self._album_info
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
@@ -637,6 +660,9 @@ class PhotoInfo:
|
||||
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
|
||||
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)
|
||||
return template.render(template_str, none_str, path_sep)
|
||||
@@ -661,6 +687,8 @@ class PhotoInfo:
|
||||
date_modified_iso = (
|
||||
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 = {
|
||||
"uuid": self.uuid,
|
||||
@@ -701,6 +729,9 @@ class PhotoInfo:
|
||||
"has_raw": self.has_raw,
|
||||
"uti_raw": self.uti_raw,
|
||||
"path_raw": self.path_raw,
|
||||
"place": self.place,
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
}
|
||||
return yaml.dump(info, sort_keys=False)
|
||||
|
||||
@@ -713,6 +744,7 @@ class PhotoInfo:
|
||||
folders = {album.title: album.folder_names for album in self.album_info}
|
||||
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
||||
place = self.place.as_dict() if self.place else {}
|
||||
score = dataclasses.asdict(self.score) if self.score else {}
|
||||
|
||||
pic = {
|
||||
"uuid": self.uuid,
|
||||
@@ -758,15 +790,22 @@ class PhotoInfo:
|
||||
"path_raw": self.path_raw,
|
||||
"place": place,
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
}
|
||||
return json.dumps(pic)
|
||||
|
||||
# compare two PhotoInfo objects for equality
|
||||
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__):
|
||||
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
|
||||
|
||||
def __ne__(self, other):
|
||||
""" Compare two PhotoInfo objects for inequality """
|
||||
return not self.__eq__(other)
|
||||
|
||||
145
osxphotos/photosdb/_photosdb_process_scoreinfo.py
Normal file
145
osxphotos/photosdb/_photosdb_process_scoreinfo.py
Normal file
@@ -0,0 +1,145 @@
|
||||
""" Methods for PhotosDB to add Photos 5 photo score info
|
||||
ref: https://simonwillison.net/2020/May/21/dogsheep-photos/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from .._constants import _PHOTOS_4_VERSION
|
||||
from ..utils import _open_sql_file
|
||||
|
||||
"""
|
||||
This module should be imported in the class defintion of PhotosDB in photosdb.py
|
||||
Do not import this module directly
|
||||
This module adds the following method to PhotosDB:
|
||||
_process_scoreinfo: process photo score info
|
||||
|
||||
The following data structures are added to PhotosDB
|
||||
self._db_scoreinfo_uuid
|
||||
|
||||
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
|
||||
"""
|
||||
|
||||
|
||||
def _process_scoreinfo(self):
|
||||
""" Process computed photo scores
|
||||
Note: Only works on Photos version == 5.0
|
||||
"""
|
||||
|
||||
# _db_scoreinfo_uuid is dict in form {uuid: {score values}}
|
||||
self._db_scoreinfo_uuid = {}
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
raise NotImplementedError(
|
||||
f"search info not implemented for this database version"
|
||||
)
|
||||
else:
|
||||
_process_scoreinfo_5(self)
|
||||
|
||||
|
||||
def _process_scoreinfo_5(photosdb):
|
||||
""" Process computed photo scores for Photos 5 databases
|
||||
|
||||
Args:
|
||||
photosdb: an OSXPhotosDB instance
|
||||
"""
|
||||
|
||||
db = photosdb._tmp_db
|
||||
|
||||
(conn, cursor) = _open_sql_file(db)
|
||||
|
||||
result = cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
ZGENERICASSET.ZUUID,
|
||||
ZGENERICASSET.ZOVERALLAESTHETICSCORE,
|
||||
ZGENERICASSET.ZCURATIONSCORE,
|
||||
ZGENERICASSET.ZPROMOTIONSCORE,
|
||||
ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
|
||||
ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZCOMPUTEDASSETATTRIBUTES ON ZCOMPUTEDASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 ZGENERICASSET.ZUUID,
|
||||
# 1 ZGENERICASSET.ZOVERALLAESTHETICSCORE,
|
||||
# 2 ZGENERICASSET.ZCURATIONSCORE,
|
||||
# 3 ZGENERICASSET.ZPROMOTIONSCORE,
|
||||
# 4 ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
|
||||
# 5 ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
|
||||
# 6 ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
|
||||
# 7 ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
|
||||
# 8 ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
|
||||
# 9 ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
|
||||
# 10 ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
|
||||
# 11 ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
|
||||
# 12 ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
|
||||
# 13 ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
|
||||
# 14 ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
|
||||
# 15 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
|
||||
# 16 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
|
||||
# 17 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
|
||||
# 18 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
|
||||
# 19 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
|
||||
# 20 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
|
||||
# 21 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
|
||||
# 22 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
|
||||
# 23 ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
|
||||
# 24 ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
|
||||
# 25 ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
|
||||
# 26 ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
|
||||
# 27 ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
|
||||
|
||||
for row in result:
|
||||
uuid = row[0]
|
||||
scores = {"uuid": uuid}
|
||||
scores["overall_aesthetic"] = row[1]
|
||||
scores["curation"] = row[2]
|
||||
scores["promotion"] = row[3]
|
||||
scores["highlight_visibility"] = row[4]
|
||||
scores["behavioral"] = row[5]
|
||||
scores["failure"] = row[6]
|
||||
scores["harmonious_color"] = row[7]
|
||||
scores["immersiveness"] = row[8]
|
||||
scores["interaction"] = row[9]
|
||||
scores["interesting_subject"] = row[10]
|
||||
scores["intrusive_object_presence"] = row[11]
|
||||
scores["lively_color"] = row[12]
|
||||
scores["low_light"] = row[13]
|
||||
scores["noise"] = row[14]
|
||||
scores["pleasant_camera_tilt"] = row[15]
|
||||
scores["pleasant_composition"] = row[16]
|
||||
scores["pleasant_lighting"] = row[17]
|
||||
scores["pleasant_pattern"] = row[18]
|
||||
scores["pleasant_perspective"] = row[19]
|
||||
scores["pleasant_post_processing"] = row[20]
|
||||
scores["pleasant_reflection"] = row[21]
|
||||
scores["pleasant_symmetry"] = row[22]
|
||||
scores["sharply_focused_subject"] = row[23]
|
||||
scores["tastefully_blurred"] = row[24]
|
||||
scores["well_chosen_subject"] = row[25]
|
||||
scores["well_framed_subject"] = row[26]
|
||||
scores["well_timed_shot"] = row[27]
|
||||
photosdb._db_scoreinfo_uuid[uuid] = scores
|
||||
@@ -102,7 +102,7 @@ def _process_searchinfo(self):
|
||||
# 8: groups.lookup_identifier
|
||||
|
||||
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
|
||||
record = {}
|
||||
record["uuid"] = uuid
|
||||
@@ -123,13 +123,9 @@ def _process_searchinfo(self):
|
||||
|
||||
category = record["category"]
|
||||
try:
|
||||
_db_searchinfo_categories[category].append(
|
||||
record["normalized_string"]
|
||||
)
|
||||
_db_searchinfo_categories[category].append(record["normalized_string"])
|
||||
except KeyError:
|
||||
_db_searchinfo_categories[category] = [
|
||||
record["normalized_string"]
|
||||
]
|
||||
_db_searchinfo_categories[category] = [record["normalized_string"]]
|
||||
|
||||
if category == SEARCH_CATEGORY_LABEL:
|
||||
label = record["content_string"]
|
||||
@@ -198,6 +194,7 @@ def labels_normalized_as_dict(self):
|
||||
|
||||
# The following method is not imported into PhotosDB
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def ints_to_uuid(uuid_0, uuid_1):
|
||||
""" convert two signed ints into a UUID strings
|
||||
|
||||
@@ -46,8 +46,6 @@ from ..utils import (
|
||||
|
||||
|
||||
# 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 special albums and magic albums
|
||||
|
||||
@@ -64,6 +62,7 @@ class PhotosDB:
|
||||
labels_as_dict,
|
||||
labels_normalized_as_dict,
|
||||
)
|
||||
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
||||
|
||||
def __init__(self, *dbfile_, dbfile=None):
|
||||
""" create a new PhotosDB object
|
||||
@@ -254,7 +253,7 @@ class PhotosDB:
|
||||
if _db_is_locked(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 int(self._db_version) > int(_PHOTOS_4_VERSION):
|
||||
@@ -312,18 +311,16 @@ class PhotosDB:
|
||||
def albums_as_dict(self):
|
||||
""" return albums as dict of albums, count in reverse sorted order (descending) """
|
||||
albums = {}
|
||||
album_keys = [
|
||||
k
|
||||
for k in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
|
||||
and not self._dbalbum_details[k]["intrash"]
|
||||
]
|
||||
for k in album_keys:
|
||||
title = self._dbalbum_details[k]["title"]
|
||||
if title in albums:
|
||||
albums[title] += len(self._dbalbums_album[k])
|
||||
album_keys = self._get_album_uuids(shared=False)
|
||||
for album in album_keys:
|
||||
title = self._dbalbum_details[album]["title"]
|
||||
if album in self._dbalbums_album:
|
||||
try:
|
||||
albums[title] += len(self._dbalbums_album[album])
|
||||
except KeyError:
|
||||
albums[title] = len(self._dbalbums_album[album])
|
||||
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))
|
||||
return albums
|
||||
|
||||
@@ -332,25 +329,17 @@ class PhotosDB:
|
||||
""" 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 """
|
||||
|
||||
# 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 = {}
|
||||
album_keys = [
|
||||
k
|
||||
for k in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is not None
|
||||
]
|
||||
for k in album_keys:
|
||||
title = self._dbalbum_details[k]["title"]
|
||||
if title in albums:
|
||||
albums[title] += len(self._dbalbums_album[k])
|
||||
album_keys = self._get_album_uuids(shared=True)
|
||||
for album in album_keys:
|
||||
title = self._dbalbum_details[album]["title"]
|
||||
if album in self._dbalbums_album:
|
||||
try:
|
||||
albums[title] += len(self._dbalbums_album[album])
|
||||
except KeyError:
|
||||
albums[title] = len(self._dbalbums_album[album])
|
||||
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))
|
||||
return albums
|
||||
|
||||
@@ -411,32 +400,28 @@ class PhotosDB:
|
||||
@property
|
||||
def album_info(self):
|
||||
""" return list of AlbumInfo objects for each album in the photos database """
|
||||
|
||||
return [
|
||||
AlbumInfo(db=self, uuid=album)
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
]
|
||||
try:
|
||||
return self._album_info
|
||||
except AttributeError:
|
||||
self._album_info = [
|
||||
AlbumInfo(db=self, uuid=album)
|
||||
for album in self._get_album_uuids(shared=False)
|
||||
]
|
||||
return self._album_info
|
||||
|
||||
@property
|
||||
def album_info_shared(self):
|
||||
""" 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 """
|
||||
# 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 []
|
||||
|
||||
return [
|
||||
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"]
|
||||
]
|
||||
try:
|
||||
return self._album_info_shared
|
||||
except AttributeError:
|
||||
self._album_info_shared = [
|
||||
AlbumInfo(db=self, uuid=album)
|
||||
for album in self._get_album_uuids(shared=True)
|
||||
]
|
||||
return self._album_info_shared
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
@@ -445,13 +430,11 @@ class PhotosDB:
|
||||
# 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
|
||||
|
||||
albums = {
|
||||
self._dbalbum_details[album]["title"]
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
}
|
||||
return list(albums)
|
||||
try:
|
||||
return self._albums
|
||||
except AttributeError:
|
||||
self._albums = self._get_albums(shared=False)
|
||||
return self._albums
|
||||
|
||||
@property
|
||||
def albums_shared(self):
|
||||
@@ -463,19 +446,11 @@ class PhotosDB:
|
||||
|
||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.warning(
|
||||
f"album_names_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
||||
)
|
||||
return []
|
||||
|
||||
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)
|
||||
try:
|
||||
return self._albums_shared
|
||||
except AttributeError:
|
||||
self._albums_shared = self._get_albums(shared=True)
|
||||
return self._albums_shared
|
||||
|
||||
@property
|
||||
def db_version(self):
|
||||
@@ -492,6 +467,14 @@ class PhotosDB:
|
||||
""" returns path to the Photos library PhotosDB was initialized with """
|
||||
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):
|
||||
""" copies the sqlite database file to a temp file """
|
||||
""" returns the name of the temp file """
|
||||
@@ -516,12 +499,18 @@ class PhotosDB:
|
||||
|
||||
return dest_path
|
||||
|
||||
def _get_db_version(self):
|
||||
""" gets the Photos DB version from LiGlobals table """
|
||||
""" returns the version as str"""
|
||||
def _get_db_version(self, db_file):
|
||||
""" Gets the Photos DB version from LiGlobals table
|
||||
|
||||
Args:
|
||||
db_file: path to database file containing LiGlobals table
|
||||
|
||||
Returns: version as str
|
||||
"""
|
||||
|
||||
version = None
|
||||
|
||||
(conn, c) = _open_sql_file(self._tmp_db)
|
||||
(conn, c) = _open_sql_file(db_file)
|
||||
|
||||
# get database version
|
||||
c.execute(
|
||||
@@ -621,6 +610,8 @@ class PhotosDB:
|
||||
"folderUuid": album[5],
|
||||
"albumType": album[6],
|
||||
"albumSubclass": album[7],
|
||||
# for compatability with Photos 5 where album kind is ZKIND
|
||||
"kind": album[7],
|
||||
}
|
||||
|
||||
# get details about folders
|
||||
@@ -1862,6 +1853,9 @@ class PhotosDB:
|
||||
# process exif info
|
||||
self._process_exifinfo()
|
||||
|
||||
# process computed scores
|
||||
self._process_scoreinfo()
|
||||
|
||||
# done processing, dump debug data if requested
|
||||
if _debug():
|
||||
logging.debug("Faces (_dbfaces_uuid):")
|
||||
@@ -2082,6 +2076,65 @@ class PhotosDB:
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
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 names
|
||||
"""
|
||||
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 (
|
||||
# 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
|
||||
(version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
|
||||
or not version4
|
||||
)
|
||||
):
|
||||
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(
|
||||
self,
|
||||
keywords=None,
|
||||
|
||||
@@ -35,6 +35,14 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{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.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.year}": "4-digit year of file modification time",
|
||||
"{modified.yy}": "2-digit year of file modification time",
|
||||
@@ -43,6 +51,14 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the file modification time",
|
||||
"{modified.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.hour}": "2-digit hour of the file modification time",
|
||||
"{modified.min}": "2-digit minute of the file modification time",
|
||||
"{modified.sec}": "2-digit second of the file modification time",
|
||||
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
# + "If used with no template will return null value. "
|
||||
# + "See https://strftime.org/ for help on strftime templates.",
|
||||
"{place.name}": "Place name from the photo's reverse geolocation data, as displayed in Photos",
|
||||
"{place.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",
|
||||
@@ -87,10 +103,17 @@ class PhotoTemplate:
|
||||
self.photo = photo
|
||||
|
||||
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
|
||||
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:
|
||||
path_sep = os.path.sep
|
||||
elif path_sep is not None and len(path_sep) != 1:
|
||||
@@ -107,7 +130,7 @@ class PhotoTemplate:
|
||||
# regex to find {template_field,optional_default} in strings
|
||||
# for explanation of regex see https://regex101.com/r/4JJg42/1
|
||||
# pylint: disable=anomalous-backslash-in-string
|
||||
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-. ]+))?)(?=\}(?!\}))\}"
|
||||
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-\%. ]+))?)(?=\}(?!\}))\}"
|
||||
if type(template) is not str:
|
||||
raise TypeError(f"template must be type str, not {type(template)}")
|
||||
|
||||
@@ -122,7 +145,7 @@ class PhotoTemplate:
|
||||
groups = len(matchobj.groups())
|
||||
if groups == 4:
|
||||
try:
|
||||
val = get_func(matchobj.group(1))
|
||||
val = get_func(matchobj.group(1), matchobj.group(3))
|
||||
except ValueError:
|
||||
return matchobj.group(0)
|
||||
|
||||
@@ -172,7 +195,7 @@ class PhotoTemplate:
|
||||
rendered_strings = set([rendered])
|
||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||
# 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)
|
||||
|
||||
# holds each of the new rendered_strings, set() to avoid duplicates
|
||||
@@ -183,10 +206,11 @@ class PhotoTemplate:
|
||||
values = self.get_template_value_multi(field, path_sep)
|
||||
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
|
||||
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:
|
||||
return val
|
||||
else:
|
||||
@@ -220,11 +244,12 @@ class PhotoTemplate:
|
||||
|
||||
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)
|
||||
|
||||
Args:
|
||||
field: template field to find value for.
|
||||
default: the default value provided by the user
|
||||
|
||||
Returns:
|
||||
The matching template value (which may be None).
|
||||
@@ -273,6 +298,24 @@ class PhotoTemplate:
|
||||
if field == "created.doy":
|
||||
return DateTimeFormatter(self.photo.date).doy
|
||||
|
||||
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 (
|
||||
DateTimeFormatter(self.photo.date_modified).date
|
||||
@@ -329,6 +372,38 @@ class PhotoTemplate:
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.hour":
|
||||
return (
|
||||
DateTimeFormatter(self.photo.date_modified).hour
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.min":
|
||||
return (
|
||||
DateTimeFormatter(self.photo.date_modified).min
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.sec":
|
||||
return (
|
||||
DateTimeFormatter(self.photo.date_modified).sec
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
# TODO: disabling modified.strftime for now because now clean way to pass
|
||||
# a default value if modified time is None
|
||||
# if field == "modified.strftime":
|
||||
# if default and self.photo.date_modified:
|
||||
# try:
|
||||
# return self.photo.date_modified.strftime(default)
|
||||
# except:
|
||||
# raise ValueError(f"Invalid strftime template: '{default}'")
|
||||
# else:
|
||||
# return None
|
||||
|
||||
if field == "place.name":
|
||||
return self.photo.place.name if self.photo.place else None
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ def dd_to_dms_str(lat, lon):
|
||||
|
||||
def get_system_library_path():
|
||||
""" 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 """
|
||||
_, major, _ = _get_os_version()
|
||||
if int(major) < 15:
|
||||
@@ -166,16 +166,10 @@ def get_system_library_path():
|
||||
with open(plist_file, "rb") as fp:
|
||||
pl = plistload(fp)
|
||||
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
|
||||
|
||||
photospath = pl["SystemLibraryPath"]
|
||||
|
||||
if photospath is not None:
|
||||
return photospath
|
||||
else:
|
||||
logging.warning("Could not get path to Photos database")
|
||||
return None
|
||||
return pl.get("SystemLibraryPath")
|
||||
|
||||
|
||||
def get_last_library_path():
|
||||
@@ -194,7 +188,7 @@ def get_last_library_path():
|
||||
|
||||
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
|
||||
# this is a serialized CFData object
|
||||
photosurlref = pl["IPXDefaultLibraryURLBookmark"]
|
||||
photosurlref = pl.get("IPXDefaultLibraryURLBookmark")
|
||||
|
||||
if photosurlref is not None:
|
||||
# use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef
|
||||
|
||||
@@ -16,7 +16,7 @@ KEYWORDS = [
|
||||
"United Kingdom",
|
||||
]
|
||||
PERSONS = ["Katie", "Suzy", "Maria"]
|
||||
ALBUMS = ["Pumpkin Farm", "Last Import", "AlbumInFolder"]
|
||||
ALBUMS = ["Pumpkin Farm", "AlbumInFolder"]
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
"wedding": 2,
|
||||
@@ -29,7 +29,7 @@ KEYWORDS_DICT = {
|
||||
"United Kingdom": 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():
|
||||
|
||||
@@ -229,6 +229,7 @@ def test_albums_photos():
|
||||
|
||||
|
||||
def test_photoinfo_albums():
|
||||
""" Test PhotoInfo.albums """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
@@ -238,7 +239,20 @@ def test_photoinfo_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():
|
||||
""" test PhotoInfo.album_info """
|
||||
|
||||
import osxphotos
|
||||
|
||||
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[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[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]
|
||||
|
||||
@@ -28,6 +28,7 @@ ALBUMS = [
|
||||
"AlbumInFolder",
|
||||
"Raw",
|
||||
"I have a deleted twin", # there's an empty album with same name that has been deleted
|
||||
"EmptyAlbum",
|
||||
]
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
@@ -47,6 +48,7 @@ ALBUM_DICT = {
|
||||
"AlbumInFolder": 2,
|
||||
"Raw": 4,
|
||||
"I have a deleted twin": 1,
|
||||
"EmptyAlbum": 0,
|
||||
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
|
||||
UUID_DICT = {
|
||||
@@ -63,6 +65,7 @@ UUID_DICT = {
|
||||
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||
"export_tif": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||
"in_album": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
@@ -439,6 +442,26 @@ def test_get_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():
|
||||
# test basic export
|
||||
# get an unedited image and export it using default filename
|
||||
@@ -777,11 +800,29 @@ def test_export_14(caplog):
|
||||
|
||||
|
||||
def test_eq():
|
||||
""" Test equality of two PhotoInfo objects """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos1 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
photos2 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
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]
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
" help Print help; for help on commands: help <command>.",
|
||||
" info Print out descriptive info of the Photos library database.",
|
||||
" keywords Print out keywords found in the Photos library.",
|
||||
" labels Print out image classification labels found in the Photos",
|
||||
" list Print list of Photos libraries found on the system.",
|
||||
" persons Print out persons (faces) found in the Photos library.",
|
||||
" places Print out places found in the Photos library.",
|
||||
@@ -210,6 +211,63 @@ CLI_EXIFTOOL = {
|
||||
"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
|
||||
try:
|
||||
exiftool = get_exiftool_path()
|
||||
@@ -224,9 +282,10 @@ def test_osxphotos():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [])
|
||||
output = result.output
|
||||
|
||||
assert result.exit_code == 0
|
||||
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
||||
assert line in output
|
||||
assert line.strip() in output
|
||||
|
||||
|
||||
def test_osxphotos_help_1():
|
||||
@@ -239,7 +298,7 @@ def test_osxphotos_help_1():
|
||||
output = result.output
|
||||
assert result.exit_code == 0
|
||||
for line in CLI_OUTPUT_NO_SUBCOMMAND:
|
||||
assert line in output
|
||||
assert line.strip() in output
|
||||
|
||||
|
||||
def test_osxphotos_help_2():
|
||||
@@ -249,7 +308,6 @@ def test_osxphotos_help_2():
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["help", "persons"])
|
||||
output = result.output
|
||||
assert result.exit_code == 0
|
||||
assert "Print out persons (faces) found in the Photos library." in result.output
|
||||
|
||||
@@ -261,7 +319,6 @@ def test_osxphotos_help_3():
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["help", "foo"])
|
||||
output = result.output
|
||||
assert result.exit_code == 0
|
||||
assert "Invalid command: foo" in result.output
|
||||
|
||||
@@ -516,14 +573,387 @@ def test_query_date():
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
import logging
|
||||
|
||||
logging.warning(result.output)
|
||||
|
||||
json_got = json.loads(result.output)
|
||||
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():
|
||||
import glob
|
||||
import os
|
||||
@@ -1260,8 +1690,6 @@ def test_export_sidecar_keyword_template():
|
||||
"EXIF:ModifyDate": "2020:04:11 12:34:16"}]"""
|
||||
)[0]
|
||||
|
||||
import logging
|
||||
|
||||
json_file = open("Pumkins2.json", "r")
|
||||
json_got = json.load(json_file)[0]
|
||||
json_file.close()
|
||||
@@ -1311,6 +1739,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")
|
||||
def test_export_update_exiftool():
|
||||
""" test export then update with exiftool """
|
||||
@@ -1626,3 +2107,79 @@ def test_export_directory_template_1_dry_run():
|
||||
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1:
|
||||
assert f"Exported {filepath}" in result.output
|
||||
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
|
||||
|
||||
97
tests/test_score_info.py
Normal file
97
tests/test_score_info.py
Normal file
@@ -0,0 +1,97 @@
|
||||
""" Test ScoreInfo """
|
||||
|
||||
from math import isclose
|
||||
import pytest
|
||||
|
||||
from osxphotos.photoinfo import ScoreInfo
|
||||
|
||||
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
|
||||
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
||||
|
||||
SCORE_DICT = {
|
||||
"4D521201-92AC-43E5-8F7C-59BC41C37A96": ScoreInfo(
|
||||
overall=0.470703125,
|
||||
curation=0.5,
|
||||
promotion=0.0,
|
||||
highlight_visibility=0.03816793893129771,
|
||||
behavioral=0.0,
|
||||
failure=-0.0006928443908691406,
|
||||
harmonious_color=0.017852783203125,
|
||||
immersiveness=0.003086090087890625,
|
||||
interaction=0.019999999552965164,
|
||||
interesting_subject=-0.0885009765625,
|
||||
intrusive_object_presence=-0.037872314453125,
|
||||
lively_color=0.10540771484375,
|
||||
low_light=0.00824737548828125,
|
||||
noise=-0.015655517578125,
|
||||
pleasant_camera_tilt=-0.006256103515625,
|
||||
pleasant_composition=0.028564453125,
|
||||
pleasant_lighting=-0.00439453125,
|
||||
pleasant_pattern=0.09088134765625,
|
||||
pleasant_perspective=0.11859130859375,
|
||||
pleasant_post_processing=0.00698089599609375,
|
||||
pleasant_reflection=-0.01523590087890625,
|
||||
pleasant_symmetry=0.01242828369140625,
|
||||
sharply_focused_subject=0.08538818359375,
|
||||
tastefully_blurred=0.022125244140625,
|
||||
well_chosen_subject=0.05596923828125,
|
||||
well_framed_subject=0.5986328125,
|
||||
well_timed_shot=0.0134124755859375,
|
||||
),
|
||||
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": ScoreInfo(
|
||||
overall=0.853515625,
|
||||
curation=0.75,
|
||||
promotion=0.0,
|
||||
highlight_visibility=0.05725190839694656,
|
||||
behavioral=0.0,
|
||||
failure=-0.0004916191101074219,
|
||||
harmonious_color=0.382080078125,
|
||||
immersiveness=0.0133209228515625,
|
||||
interaction=0.03999999910593033,
|
||||
interesting_subject=0.1632080078125,
|
||||
intrusive_object_presence=-0.00966644287109375,
|
||||
lively_color=0.44091796875,
|
||||
low_light=0.01322174072265625,
|
||||
noise=-0.0026721954345703125,
|
||||
pleasant_camera_tilt=0.028045654296875,
|
||||
pleasant_composition=0.33642578125,
|
||||
pleasant_lighting=0.46142578125,
|
||||
pleasant_pattern=0.1944580078125,
|
||||
pleasant_perspective=0.494384765625,
|
||||
pleasant_post_processing=0.4970703125,
|
||||
pleasant_reflection=0.00910186767578125,
|
||||
pleasant_symmetry=0.00930023193359375,
|
||||
sharply_focused_subject=0.52490234375,
|
||||
tastefully_blurred=0.63916015625,
|
||||
well_chosen_subject=0.64208984375,
|
||||
well_framed_subject=0.485595703125,
|
||||
well_timed_shot=0.01531219482421875,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def photosdb():
|
||||
import osxphotos
|
||||
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_5)
|
||||
|
||||
|
||||
def test_score_info_v5(photosdb):
|
||||
""" test score """
|
||||
# use math.isclose to compare floats
|
||||
# on MacOS x64 these can probably compared for equality but would possibly
|
||||
# fail if osxphotos ever ported to other platforms
|
||||
for uuid in SCORE_DICT:
|
||||
photo = photosdb.photos(uuid=[uuid], movies=True)[0]
|
||||
for attr in photo.score.__dict__:
|
||||
assert isclose(getattr(photo.score, attr), getattr(SCORE_DICT[uuid], attr))
|
||||
|
||||
|
||||
def test_score_info_v4():
|
||||
""" test version 4, score should be None """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
|
||||
for photo in photosdb.photos():
|
||||
assert photo.score is None
|
||||
@@ -32,6 +32,9 @@ TEMPLATE_VALUES = {
|
||||
"{created.dd}": "04",
|
||||
"{created.dow}": "Tuesday",
|
||||
"{created.doy}": "035",
|
||||
"{created.hour}": "19",
|
||||
"{created.min}": "07",
|
||||
"{created.sec}": "38",
|
||||
"{modified.date}": "2020-03-21",
|
||||
"{modified.year}": "2020",
|
||||
"{modified.yy}": "20",
|
||||
@@ -40,6 +43,9 @@ TEMPLATE_VALUES = {
|
||||
"{modified.mon}": "Mar",
|
||||
"{modified.dd}": "21",
|
||||
"{modified.doy}": "081",
|
||||
"{modified.hour}": "01",
|
||||
"{modified.min}": "33",
|
||||
"{modified.sec}": "08",
|
||||
"{place.name}": "Washington, District of Columbia, United States",
|
||||
"{place.country_code}": "US",
|
||||
"{place.name.country}": "United States",
|
||||
@@ -106,7 +112,7 @@ def test_lookup():
|
||||
|
||||
for subst in TEMPLATE_SUBSTITUTIONS:
|
||||
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
|
||||
|
||||
|
||||
@@ -115,7 +121,10 @@ def test_lookup_multi():
|
||||
import os
|
||||
import re
|
||||
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)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
@@ -123,10 +132,11 @@ def test_lookup_multi():
|
||||
|
||||
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
||||
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 len(lookup) >= 1
|
||||
|
||||
|
||||
def test_subst():
|
||||
""" Test that substitutions are correct """
|
||||
import locale
|
||||
@@ -432,3 +442,19 @@ def test_subst_multi_folder_albums_3():
|
||||
rendered, unknown = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
assert unknown == []
|
||||
|
||||
|
||||
def test_subst_strftime():
|
||||
""" Test that strftime substitutions are correct """
|
||||
import locale
|
||||
import osxphotos
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
|
||||
assert rendered[0] == "2020-02-04-190738"
|
||||
|
||||
rendered, unmatched = photo.render_template("{created.strftime}")
|
||||
assert rendered[0] == "_"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" Builds the template table in markdown format for README.md """
|
||||
|
||||
from osxphotos.photoinfo.template import (
|
||||
from osxphotos.phototemplate import (
|
||||
TEMPLATE_SUBSTITUTIONS,
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user