Compare commits

...

51 Commits

Author SHA1 Message Date
Rhet Turnbull
db27aac14b Added test for #409 2021-04-02 21:44:45 -07:00
Rhet Turnbull
d17454772c Update phototemplate.py
Fix for non-str values in exiftool template (#409)
2021-03-30 07:51:34 -06:00
dependabot[bot]
9c9e73ba96 Bump pygments from 2.6.1 to 2.7.4 (#408)
Bumps [pygments](https://github.com/pygments/pygments) from 2.6.1 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.6.1...2.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 07:37:28 -06:00
Rhet Turnbull
e21a78c2b3 Removed logging.debug code 2021-03-28 07:05:29 -07:00
Rhet Turnbull
de0fbf2bb9 Updated CHANGELOG.md, [skip ci] 2021-03-28 06:44:53 -07:00
Rhet Turnbull
b330e27fb8 Added --retry, issue #406 2021-03-27 22:40:56 -07:00
Rhet Turnbull
a941f66d62 Fixed albums for burst images, closes #401, #403, #404 2021-03-27 08:11:33 -07:00
dependabot[bot]
d77eba12b2 Bump pyyaml from 5.1.2 to 5.4 (#402)
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.1.2 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.1.2...5.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-25 21:06:24 -07:00
Rhet Turnbull
de94fd76de Updated CHANGELOG.md, [skip ci] 2021-03-21 23:18:41 -07:00
Rhet Turnbull
1026473684 Added --from-time, --to-time, closes #400 2021-03-21 22:57:18 -07:00
dependabot[bot]
3f9c9893c3 Bump pillow from 7.2.0 to 8.1.1 (#399)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.2.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.2.0...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-19 08:15:36 -07:00
Rhet Turnbull
574cdd65a3 Updated CHANGELOG.md, [skip ci] 2021-03-16 07:02:35 -07:00
Rhet Turnbull
5b9547669e Add --cleanup files to report, #395 2021-03-14 14:58:35 -07:00
allcontributors[bot]
35b5bbd13d docs: add AaronVanGeffen as a contributor (#398)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-03-14 12:19:26 -07:00
Aaron van Geffen
6870ad0d8e Use original filename to export photos by default (#396)
* Use original filename to export photos by default, #176

* Fixed edited filename to also use original_filename if none provided.

* Rolled back previous change due to test failures

* Use original filename for edited file exports as well

Co-authored-by: Rhet Turnbull <rturnbull@gmail.com>
2021-03-14 12:19:05 -07:00
Rhet Turnbull
17ac5949e1 Updated docs for --cleanup, #394 2021-03-14 12:16:26 -07:00
Rhet Turnbull
ffb9af1965 Fix for long descriptions with exiftool, #393 2021-03-14 08:38:07 -07:00
Rhet Turnbull
595307a003 Bug fix, convert PosixPath to str, #392 2021-03-04 23:46:25 -08:00
Rhet Turnbull
79a50b9e50 Updated CHANGELOG.md, [skip ci] 2021-02-21 23:13:56 -08:00
Rhet Turnbull
515df0a5dc Template refactor (#385)
* Initial implementation of new textx parser for template

* Implemented parser as singleton

* Moved grammar to .tx file

* Added filter templates

* Added filter templates

* Added tests for nested templates

* Added tests for filter+path_sep

* Added tests for filter+path_sep

* Added punctuation templates

* Added hook for --replace-keywords

* Updated docs for phototemplate

* Updated docs for phototemplate

* Updated tests data

* Updated tests data

* Updated docs for phototemplate

* Version bump

* Updated CLI help

* Fixed template processing for boolean, default
2021-02-21 20:19:51 -08:00
Rhet Turnbull
63bfa92563 Updated CHANGELOG.md, [skip ci] 2021-02-20 12:49:24 -08:00
Rhet Turnbull
44a1e3e7a7 Better exception handling for AdjustmentsInfo 2021-02-20 12:28:19 -08:00
Rhet Turnbull
6c84e476cc Updated CHANGELOG.md, [skip ci] 2021-02-20 11:35:15 -08:00
Rhet Turnbull
14fbe5e068 Merge pull request #383 from RhetTbull/all-contributors/add-neilpa
docs: add neilpa as a contributor
2021-02-20 11:05:19 -08:00
allcontributors[bot]
ebac9d0bfb docs: update .all-contributorsrc [skip ci] 2021-02-20 19:04:05 +00:00
allcontributors[bot]
29716c5272 docs: update README.md [skip ci] 2021-02-20 19:04:04 +00:00
Rhet Turnbull
fbe8229103 Version bump 2021-02-20 11:01:58 -08:00
Rhet Turnbull
5ee6affc05 Added AdjustmentsInfo, #150, #379 2021-02-20 11:01:08 -08:00
Rhet Turnbull
b3a7869bd3 Added depth_state to _info 2021-02-17 21:37:22 -08:00
Rhet Turnbull
e5f1c29974 Updated docs for --ignore-signature, #286 2021-02-14 11:37:32 -08:00
Rhet Turnbull
70848e1ff6 Removed orientation from XMP, #378 2021-02-13 18:58:33 -08:00
Rhet Turnbull
4b7a53faa8 Write description to ITPC:CaptionAbstract (#380) 2021-02-13 10:37:01 -08:00
Rhet Turnbull
a78dd80af4 Updated CHANGELOG.md, [skip ci] 2021-02-12 18:52:06 -08:00
Rhet Turnbull
1316866dc4 Added image orientation bug to Known Bugs 2021-02-12 08:24:24 -08:00
Rhet Turnbull
30273509d4 Fix for issue #366, --jpeg-ext, --convert-to-jpeg bug 2021-02-12 07:52:18 -08:00
Rhet Turnbull
15a3e69015 Updated CHANGELOG.md, [skip ci] 2021-02-10 06:50:56 -08:00
Rhet Turnbull
2691902d5c Added test for #374 2021-02-10 06:50:40 -08:00
Rhet Turnbull
da47821fae Bug fix for --jpeg-ext, #374 2021-02-09 22:17:20 -08:00
Rhet Turnbull
6f38e2da49 Updated CHANGELOG.md, [skip ci] 2021-02-08 22:07:17 -08:00
Rhet Turnbull
857e3db6cc Fixed --exiftool-option, #369, for real this time 2021-02-08 21:59:20 -08:00
Rhet Turnbull
7ed3115f36 Updated CHANGELOG.md, [skip ci] 2021-02-08 21:31:21 -08:00
Rhet Turnbull
198addaa07 Fixed --exiftool-option, #369 2021-02-08 21:21:30 -08:00
Rhet Turnbull
d91fc93737 Updated CHANGELOG.md, [skip ci] 2021-02-07 09:34:00 -08:00
Rhet Turnbull
5c3360f29d Fix for issue #366 2021-02-07 09:26:19 -08:00
Rhet Turnbull
d4513832a6 Updated CHANGELOG.md, [skip ci] 2021-02-07 09:05:59 -08:00
Rhet Turnbull
f8616acf16 Fixed unnecessary warning for long keywords, issue #365 2021-02-07 05:40:41 -08:00
Rhet Turnbull
addd952aa3 Implemented --in-album, --not-in-album, issue #364 2021-02-04 06:40:49 -08:00
Rhet Turnbull
773b619e24 Updated requirements.txt 2021-02-03 06:42:46 -08:00
Rhet Turnbull
683dfe7f3f Updated docs Makefile [skip ci] 2021-02-03 06:27:09 -08:00
Rhet Turnbull
7fa5fbaa5b Updated docs 2021-02-03 06:06:32 -08:00
Rhet Turnbull
e075868281 Updated CHANGELOG.md, [skip ci] 2021-02-02 22:14:41 -08:00
215 changed files with 5847 additions and 1972 deletions

View File

@@ -175,6 +175,24 @@
"contributions": [ "contributions": [
"doc" "doc"
] ]
},
{
"login": "neilpa",
"name": "Neil Pankey",
"avatar_url": "https://avatars.githubusercontent.com/u/42419?v=4",
"profile": "https://neilpa.me",
"contributions": [
"code"
]
},
{
"login": "AaronVanGeffen",
"name": "Aaron van Geffen",
"avatar_url": "https://avatars.githubusercontent.com/u/604665?v=4",
"profile": "https://aaronweb.net/",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

16
.gitignore vendored Normal file
View File

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

View File

@@ -4,6 +4,155 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.41.6](https://github.com/RhetTbull/osxphotos/compare/v0.41.5...v0.41.6)
> 28 March 2021
- Added --retry, issue #406 [`b330e27`](https://github.com/RhetTbull/osxphotos/commit/b330e27fb838b702cefcbdb588c2fbb924b4cbc4)
#### [v0.41.5](https://github.com/RhetTbull/osxphotos/compare/v0.41.4...v0.41.5)
> 27 March 2021
- Bump pyyaml from 5.1.2 to 5.4 [`#402`](https://github.com/RhetTbull/osxphotos/pull/402)
- Fixed albums for burst images, closes #401, #403, #404 [`#401`](https://github.com/RhetTbull/osxphotos/issues/401)
#### [v0.41.4](https://github.com/RhetTbull/osxphotos/compare/v0.41.3...v0.41.4)
> 22 March 2021
- Bump pillow from 7.2.0 to 8.1.1 [`#399`](https://github.com/RhetTbull/osxphotos/pull/399)
- Added --from-time, --to-time, closes #400 [`#400`](https://github.com/RhetTbull/osxphotos/issues/400)
#### [v0.41.3](https://github.com/RhetTbull/osxphotos/compare/v0.41.2...v0.41.3)
> 14 March 2021
- docs: add AaronVanGeffen as a contributor [`#398`](https://github.com/RhetTbull/osxphotos/pull/398)
- Use original filename to export photos by default [`#396`](https://github.com/RhetTbull/osxphotos/pull/396)
- Updated docs for --cleanup, #394 [`17ac594`](https://github.com/RhetTbull/osxphotos/commit/17ac5949e15057379eb13b979d4d7498bbb94d67)
- Add --cleanup files to report, #395 [`5b95476`](https://github.com/RhetTbull/osxphotos/commit/5b9547669ed6622ae06607e024315e383c0b2d98)
#### [v0.41.2](https://github.com/RhetTbull/osxphotos/compare/v0.41.1...v0.41.2)
> 14 March 2021
- Fix for long descriptions with exiftool, #393 [`ffb9af1`](https://github.com/RhetTbull/osxphotos/commit/ffb9af1965668bcfc2422f08b2462964a7dae3e2)
#### [v0.41.1](https://github.com/RhetTbull/osxphotos/compare/v0.41.0...v0.41.1)
> 5 March 2021
- Bug fix, convert PosixPath to str, #392 [`595307a`](https://github.com/RhetTbull/osxphotos/commit/595307a003c8ae5d3bee3ad161bb880d884b3cc3)
#### [v0.41.0](https://github.com/RhetTbull/osxphotos/compare/v0.40.19...v0.41.0)
> 22 February 2021
- Template refactor [`#385`](https://github.com/RhetTbull/osxphotos/pull/385)
#### [v0.40.19](https://github.com/RhetTbull/osxphotos/compare/v0.40.18...v0.40.19)
> 20 February 2021
- Better exception handling for AdjustmentsInfo [`44a1e3e`](https://github.com/RhetTbull/osxphotos/commit/44a1e3e7a7f765bf91c2341e423ec9e5a9e3c1bd)
#### [v0.40.18](https://github.com/RhetTbull/osxphotos/compare/v0.40.17...v0.40.18)
> 20 February 2021
- docs: add neilpa as a contributor [`#383`](https://github.com/RhetTbull/osxphotos/pull/383)
- Added AdjustmentsInfo, #150, #379 [`5ee6aff`](https://github.com/RhetTbull/osxphotos/commit/5ee6affc0525db1975cb5095f62494ef10d92f7e)
- docs: update .all-contributorsrc [skip ci] [`ebac9d0`](https://github.com/RhetTbull/osxphotos/commit/ebac9d0bfb43f59f046aacdd0290d1fcd29a3b5e)
- docs: update README.md [skip ci] [`29716c5`](https://github.com/RhetTbull/osxphotos/commit/29716c52726a4e699c03d43ecc67db57f55b36f8)
- Version bump [`fbe8229`](https://github.com/RhetTbull/osxphotos/commit/fbe822910370652975ab83b82344169df4c3027c)
#### [v0.40.17](https://github.com/RhetTbull/osxphotos/compare/v0.40.16...v0.40.17)
> 18 February 2021
- Updated docs for --ignore-signature, #286 [`e5f1c29`](https://github.com/RhetTbull/osxphotos/commit/e5f1c299742fcfa0a855a33df7b266aa2c39e48b)
- Added depth_state to _info [`b3a7869`](https://github.com/RhetTbull/osxphotos/commit/b3a7869bd3cc13e40cb3f68ff8caf12edda9a49c)
#### [v0.40.16](https://github.com/RhetTbull/osxphotos/compare/v0.40.14...v0.40.16)
> 14 February 2021
- Write description to ITPC:CaptionAbstract (#380) [`4b7a53f`](https://github.com/RhetTbull/osxphotos/commit/4b7a53faa8d7ff2e941e7653554f61bcbd416fc9)
- Removed orientation from XMP, #378 [`70848e1`](https://github.com/RhetTbull/osxphotos/commit/70848e1ff6def928b052271b47c1697c23a8c73f)
- Added image orientation bug to Known Bugs [`1316866`](https://github.com/RhetTbull/osxphotos/commit/1316866dc47486ac61db8903d2d7d006f2598a77)
#### [v0.40.14](https://github.com/RhetTbull/osxphotos/compare/v0.40.13...v0.40.14)
> 12 February 2021
- Fix for issue #366, --jpeg-ext, --convert-to-jpeg bug [`3027350`](https://github.com/RhetTbull/osxphotos/commit/30273509d40a270d2610b662ed9238449350064c)
- Added test for #374 [`2691902`](https://github.com/RhetTbull/osxphotos/commit/2691902d5c7a4f4f81e3a9b36fd560ff0a07aec1)
#### [v0.40.13](https://github.com/RhetTbull/osxphotos/compare/v0.40.12...v0.40.13)
> 10 February 2021
- Bug fix for --jpeg-ext, #374 [`da47821`](https://github.com/RhetTbull/osxphotos/commit/da47821fae7ee7b2d6d89f5542e729e01d3338df)
#### [v0.40.12](https://github.com/RhetTbull/osxphotos/compare/v0.40.11...v0.40.12)
> 9 February 2021
- Fixed --exiftool-option, #369, for real this time [`857e3db`](https://github.com/RhetTbull/osxphotos/commit/857e3db6ccce810d682cd4632ac9bc8448c4f86b)
#### [v0.40.11](https://github.com/RhetTbull/osxphotos/compare/v0.40.10...v0.40.11)
> 9 February 2021
- Fixed --exiftool-option, #369 [`198adda`](https://github.com/RhetTbull/osxphotos/commit/198addaa07a86ac5b0fd82787fdffff0a0fc19c6)
#### [v0.40.10](https://github.com/RhetTbull/osxphotos/compare/v0.40.9...v0.40.10)
> 7 February 2021
- Fix for issue #366 [`5c3360f`](https://github.com/RhetTbull/osxphotos/commit/5c3360f29d52df2f804c70f37a2ca9a3f102d93c)
#### [v0.40.9](https://github.com/RhetTbull/osxphotos/compare/v0.40.8...v0.40.9)
> 7 February 2021
- Fixed unnecessary warning for long keywords, issue #365 [`f8616ac`](https://github.com/RhetTbull/osxphotos/commit/f8616acf167b5e73ab3e4b68dcfbf578230c330d)
#### [v0.40.8](https://github.com/RhetTbull/osxphotos/compare/v0.40.7...v0.40.8)
> 4 February 2021
- Implemented --in-album, --not-in-album, issue #364 [`addd952`](https://github.com/RhetTbull/osxphotos/commit/addd952aa315007852945a352b2c7c451ba5f21a)
- Updated docs [`7fa5fba`](https://github.com/RhetTbull/osxphotos/commit/7fa5fbaa5b7c9aa1412eceef56e068dc044c91e0)
- Updated docs Makefile [skip ci] [`683dfe7`](https://github.com/RhetTbull/osxphotos/commit/683dfe7f3ffd235659b58f403562ce2d51123cfb)
#### [v0.40.7](https://github.com/RhetTbull/osxphotos/compare/v0.40.6...v0.40.7)
> 3 February 2021
- Bump bleach from 3.1.4 to 3.3.0 [`#362`](https://github.com/RhetTbull/osxphotos/pull/362)
- Fixed XMP template for issue #361 [`43af4d2`](https://github.com/RhetTbull/osxphotos/commit/43af4d205a7264e530bc2b2789d297be633391e1)
- Updated sidecar test data [`591f9bc`](https://github.com/RhetTbull/osxphotos/commit/591f9bcc62720f7eddebba3b3dcff265907550dd)
- Added tests for --only-new, #358 [`adc4b05`](https://github.com/RhetTbull/osxphotos/commit/adc4b056029794faddd464d22022a2a17298a924)
- Updated tests for ExportDB, #358 [`48d2223`](https://github.com/RhetTbull/osxphotos/commit/48d2223edde4850830cc6a3f9776ce08f81a6636)
- Added 11.2 to tested versions, #360 [`2284598`](https://github.com/RhetTbull/osxphotos/commit/2284598a24f63232c01dcf27b9982002123834ca)
#### [v0.40.6](https://github.com/RhetTbull/osxphotos/compare/v0.40.5...v0.40.6)
> 2 February 2021
- Add @davidjroos as a contributor [`8dbedef`](https://github.com/RhetTbull/osxphotos/commit/8dbedef1874882815afb4a885184249aae73bf9f)
- Fixed documentation, #359 [`77371b6`](https://github.com/RhetTbull/osxphotos/commit/77371b6e5d8a9b8662b7b7d540378beb897f6988)
#### [v0.40.5](https://github.com/RhetTbull/osxphotos/compare/v0.40.3...v0.40.5)
> 1 February 2021
- Restructured docs [`3a4a8bd`](https://github.com/RhetTbull/osxphotos/commit/3a4a8bdb0bdd995c937e0a15f5d8f1685b73407f)
- Refactored __main__, added sphinx docs [`51f6958`](https://github.com/RhetTbull/osxphotos/commit/51f69585be60d12f912ba08f138b9c1f74481dbd)
- Implemented --only-new, #358 [`5c093c4`](https://github.com/RhetTbull/osxphotos/commit/5c093c43528193ed1704ed4ef1b8d841a95a81cf)
#### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3) #### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3)
> 23 January 2021 > 23 January 2021

View File

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

664
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.40.4 documentation</title> <title>Overview: module code &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
@@ -93,7 +93,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -183,7 +183,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -120,6 +120,8 @@
<span class="n">exiftool_error</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">exiftool_error</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">xattr_written</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">xattr_written</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">xattr_skipped</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">xattr_skipped</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">deleted_files</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">deleted_directories</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exported</span> <span class="o">=</span> <span class="n">exported</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">exported</span> <span class="o">=</span> <span class="n">exported</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">new</span> <span class="o">=</span> <span class="n">new</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">new</span> <span class="o">=</span> <span class="n">new</span> <span class="ow">or</span> <span class="p">[]</span>
@@ -140,6 +142,8 @@
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">=</span> <span class="n">exiftool_error</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">=</span> <span class="n">exiftool_error</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">xattr_written</span> <span class="o">=</span> <span class="n">xattr_written</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">xattr_written</span> <span class="o">=</span> <span class="n">xattr_written</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">xattr_skipped</span> <span class="o">=</span> <span class="n">xattr_skipped</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">xattr_skipped</span> <span class="o">=</span> <span class="n">xattr_skipped</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span> <span class="o">=</span> <span class="n">deleted_files</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span> <span class="o">=</span> <span class="n">deleted_directories</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">all_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">all_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return all filenames contained in results &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; return all filenames contained in results &quot;&quot;&quot;</span>
@@ -184,6 +188,8 @@
<span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">error</span> <span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">error</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_warning</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_error</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">deleted_files</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">deleted_directories</span>
<span class="k">return</span> <span class="bp">self</span> <span class="k">return</span> <span class="bp">self</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -206,6 +212,8 @@
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">error</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">error</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_warning=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_warning=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_files=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_directories=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="s2">&quot;)&quot;</span> <span class="o">+</span> <span class="s2">&quot;)&quot;</span>
<span class="p">)</span> <span class="p">)</span>
@@ -508,6 +516,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">jpeg_ext</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">jpeg_ext</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span> <span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span>
<span class="sd"> dest: must be valid destination path or exception raised</span> <span class="sd"> dest: must be valid destination path or exception raised</span>
@@ -560,6 +571,9 @@
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading &quot;.&quot;</span> <span class="sd"> jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading &quot;.&quot;</span>
<span class="sd"> persons: if True, include persons in exported metadata</span>
<span class="sd"> location: if True, include location in exported metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: ExportResults class </span> <span class="sd"> Returns: ExportResults class </span>
<span class="sd"> ExportResults has attributes: </span> <span class="sd"> ExportResults has attributes: </span>
@@ -595,11 +609,11 @@
<span class="k">if</span> <span class="n">export_db</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> <span class="k">if</span> <span class="n">export_db</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">export_db</span> <span class="o">=</span> <span class="n">ExportDBNoOp</span><span class="p">()</span> <span class="n">export_db</span> <span class="o">=</span> <span class="n">ExportDBNoOp</span><span class="p">()</span>
<span class="k">if</span> <span class="n">verbose</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> <span class="k">if</span> <span class="n">verbose</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">verbose</span><span class="p">):</span>
<span class="n">verbose</span> <span class="o">=</span> <span class="n">noop</span>
<span class="k">elif</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">verbose</span><span class="p">):</span>
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">&quot;verbose must be callable&quot;</span><span class="p">)</span> <span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">&quot;verbose must be callable&quot;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="n">verbose</span>
<span class="k">if</span> <span class="n">verbose</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
<span class="c1"># suffix to add to edited files</span> <span class="c1"># suffix to add to edited files</span>
<span class="c1"># e.g. name will be filename_edited.jpg</span> <span class="c1"># e.g. name will be filename_edited.jpg</span>
@@ -640,9 +654,9 @@
<span class="p">)</span> <span class="p">)</span>
<span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span> <span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
<span class="n">edited_suffix</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">edited_name</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span> <span class="n">edited_suffix</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">edited_name</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span>
<span class="n">fname</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">filename</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="o">+</span> <span class="n">edited_identifier</span> <span class="o">+</span> <span class="n">edited_suffix</span> <span class="n">fname</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="o">+</span> <span class="n">edited_identifier</span> <span class="o">+</span> <span class="n">edited_suffix</span>
<span class="k">else</span><span class="p">:</span> <span class="k">else</span><span class="p">:</span>
<span class="n">fname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">filename</span> <span class="n">fname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span>
<span class="n">uti</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span> <span class="n">uti</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span>
<span class="k">if</span> <span class="n">convert_to_jpeg</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span> <span class="ow">and</span> <span class="n">uti</span> <span class="o">!=</span> <span class="s2">&quot;public.jpeg&quot;</span><span class="p">:</span> <span class="k">if</span> <span class="n">convert_to_jpeg</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span> <span class="ow">and</span> <span class="n">uti</span> <span class="o">!=</span> <span class="s2">&quot;public.jpeg&quot;</span><span class="p">:</span>
@@ -974,6 +988,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -997,6 +1014,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -1016,6 +1036,9 @@
<span class="n">keyword_template</span><span class="o">=</span><span class="n">keyword_template</span><span class="p">,</span> <span class="n">keyword_template</span><span class="o">=</span><span class="n">keyword_template</span><span class="p">,</span>
<span class="n">description_template</span><span class="o">=</span><span class="n">description_template</span><span class="p">,</span> <span class="n">description_template</span><span class="o">=</span><span class="n">description_template</span><span class="p">,</span>
<span class="n">extension</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="k">if</span> <span class="n">dest</span><span class="o">.</span><span class="n">suffix</span> <span class="k">else</span> <span class="kc">None</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="k">if</span> <span class="n">dest</span><span class="o">.</span><span class="n">suffix</span> <span class="k">else</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -1083,6 +1106,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span> <span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
@@ -1103,6 +1129,9 @@
<span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span> <span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span> <span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1120,6 +1149,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">),</span> <span class="p">),</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span> <span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1142,6 +1174,9 @@
<span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span> <span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span> <span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1159,6 +1194,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">),</span> <span class="p">),</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span> <span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1378,6 +1416,9 @@
<span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span> <span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span>
@@ -1388,6 +1429,9 @@
<span class="sd"> keyword_template: (list of strings); list of template strings to render as keywords</span> <span class="sd"> keyword_template: (list of strings); list of template strings to render as keywords</span>
<span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span> <span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span>
<span class="sd"> flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)</span> <span class="sd"> flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)</span>
<span class="sd"> persons: if True, write person data to metadata</span>
<span class="sd"> location: if True, write location data to metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns:</span> <span class="sd"> Returns:</span>
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span> <span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
@@ -1402,6 +1446,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span> <span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
@@ -1424,6 +1471,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span> <span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span> <span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1437,6 +1487,9 @@
<span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span> <span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span>
<span class="sd"> merge_exif_keywords: merge keywords in the file&#39;s exif metadata (requires exiftool)</span> <span class="sd"> merge_exif_keywords: merge keywords in the file&#39;s exif metadata (requires exiftool)</span>
<span class="sd"> merge_exif_persons: merge persons in the file&#39;s exif metadata (requires exiftool)</span> <span class="sd"> merge_exif_persons: merge persons in the file&#39;s exif metadata (requires exiftool)</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: dict with exiftool tags / values</span> <span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1444,8 +1497,10 @@
<span class="sd"> EXIF:ImageDescription (may include template)</span> <span class="sd"> EXIF:ImageDescription (may include template)</span>
<span class="sd"> XMP:Description (may include template)</span> <span class="sd"> XMP:Description (may include template)</span>
<span class="sd"> XMP:Title</span> <span class="sd"> XMP:Title</span>
<span class="sd"> IPTC:ObjectName</span>
<span class="sd"> XMP:TagsList (may include album name, person name, or template)</span> <span class="sd"> XMP:TagsList (may include album name, person name, or template)</span>
<span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span> <span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span>
<span class="sd"> IPTC:Caption-Abstract</span>
<span class="sd"> XMP:Subject (set to keywords + persons)</span> <span class="sd"> XMP:Subject (set to keywords + persons)</span>
<span class="sd"> XMP:PersonInImage</span> <span class="sd"> XMP:PersonInImage</span>
<span class="sd"> EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef</span> <span class="sd"> EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef</span>
@@ -1461,6 +1516,9 @@
<span class="sd"> QuickTime:ModifyDate (UTC)</span> <span class="sd"> QuickTime:ModifyDate (UTC)</span>
<span class="sd"> QuickTime:GPSCoordinates</span> <span class="sd"> QuickTime:GPSCoordinates</span>
<span class="sd"> UserData:GPSCoordinates</span> <span class="sd"> UserData:GPSCoordinates</span>
<span class="sd"> Reference: </span>
<span class="sd"> https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf</span>
<span class="sd"> &quot;&quot;&quot;</span> <span class="sd"> &quot;&quot;&quot;</span>
<span class="n">exif</span> <span class="o">=</span> <span class="p">(</span> <span class="n">exif</span> <span class="o">=</span> <span class="p">(</span>
@@ -1480,30 +1538,34 @@
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span> <span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span><span class="p">:</span> <span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:ObjectName&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="n">keyword_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">keyword_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span> <span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">replace_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span> <span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span>
<span class="c1"># filter out _UNKNOWN_PERSON</span> <span class="c1"># filter out _UNKNOWN_PERSON</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span> <span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span>
<span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span>
<span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
@@ -1534,8 +1596,8 @@
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span>
<span class="p">]</span> <span class="p">]</span>
<span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span> <span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">&quot;</span> <span class="sa">f</span><span class="s2">&quot;Warning: some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2"> (exiftool will truncate these): </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
@@ -1547,25 +1609,26 @@
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Subject&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Subject&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:TagsList&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:TagsList&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="k">if</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span> <span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:PersonInImage&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">person_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:PersonInImage&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">person_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="c1"># if self.favorite():</span> <span class="c1"># if self.favorite():</span>
<span class="c1"># exif[&quot;Rating&quot;] = 5</span> <span class="c1"># exif[&quot;Rating&quot;] = 5</span>
<span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span> <span class="k">if</span> <span class="n">location</span><span class="p">:</span>
<span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">lon</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span> <span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span> <span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">lon</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span>
<span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">&quot;N&quot;</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;S&quot;</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span>
<span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">&quot;E&quot;</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;W&quot;</span> <span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">&quot;N&quot;</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;S&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span> <span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">&quot;E&quot;</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;W&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_ref</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">:</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_ref</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;Keys:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span> <span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;UserData:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;Keys:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;UserData:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="c1"># process date/time and timezone offset</span> <span class="c1"># process date/time and timezone offset</span>
<span class="c1"># Photos exports the following fields and sets modify date to creation date</span> <span class="c1"># Photos exports the following fields and sets modify date to creation date</span>
@@ -1624,6 +1687,13 @@
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;QuickTime:ModifyDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime_tz_to_utc</span><span class="p">(</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;QuickTime:ModifyDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime_tz_to_utc</span><span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span> <span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span>
<span class="c1"># remove any new lines in any fields</span>
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span><span class="p">]</span>
<span class="k">return</span> <span class="n">exif</span> <span class="k">return</span> <span class="n">exif</span>
@@ -1673,6 +1743,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span> <span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span> <span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1687,13 +1760,18 @@
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> filename: filename of the destination image file for including in exiftool signature in JSON sidecar</span> <span class="sd"> filename: filename of the destination image file for including in exiftool signature in JSON sidecar</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: dict with exiftool tags / values</span> <span class="sd"> Returns: dict with exiftool tags / values</span>
<span class="sd"> Exports the following:</span> <span class="sd"> Exports the following:</span>
<span class="sd"> EXIF:ImageDescription</span> <span class="sd"> EXIF:ImageDescription</span>
<span class="sd"> XMP:Description (may include template)</span> <span class="sd"> XMP:Description (may include template)</span>
<span class="sd"> IPTC:CaptionAbstract</span>
<span class="sd"> XMP:Title</span> <span class="sd"> XMP:Title</span>
<span class="sd"> IPTC:ObjectName</span>
<span class="sd"> XMP:TagsList</span> <span class="sd"> XMP:TagsList</span>
<span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span> <span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span>
<span class="sd"> XMP:Subject (set to keywords + person)</span> <span class="sd"> XMP:Subject (set to keywords + person)</span>
@@ -1721,6 +1799,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">filename</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">filename</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
@@ -1743,6 +1824,9 @@
<span class="n">extension</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span> <span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span>
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span> <span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
@@ -1752,6 +1836,9 @@
<span class="sd"> extension: which extension to use for SidecarForExtension property</span> <span class="sd"> extension: which extension to use for SidecarForExtension property</span>
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> &quot;&quot;&quot;</span> <span class="sd"> &quot;&quot;&quot;</span>
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span> <span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
@@ -1775,22 +1862,23 @@
<span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span> <span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">replace_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="c1"># TODO: keyword handling in this and _exiftool_json_sidecar is</span> <span class="c1"># TODO: keyword handling in this and _exiftool_json_sidecar is</span>
<span class="c1"># good candidate for pulling out in a function</span> <span class="c1"># good candidate for pulling out in a function</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span> <span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span>
<span class="c1"># filter out _UNKNOWN_PERSON</span> <span class="c1"># filter out _UNKNOWN_PERSON</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span> <span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span>
<span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span>
<span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
@@ -1814,28 +1902,20 @@
<span class="k">if</span> <span class="n">_OSXPHOTOS_NONE_SENTINEL</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">keyword</span> <span class="k">if</span> <span class="n">_OSXPHOTOS_NONE_SENTINEL</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">keyword</span>
<span class="p">]</span> <span class="p">]</span>
<span class="c1"># check to see if any keywords too long</span>
<span class="n">long_keywords</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">long_str</span>
<span class="k">for</span> <span class="n">long_str</span> <span class="ow">in</span> <span class="n">rendered_keywords</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span>
<span class="p">]</span>
<span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="p">)</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
<span class="c1"># remove duplicates</span> <span class="c1"># remove duplicates</span>
<span class="c1"># sorted mainly to make testing the XMP file easier</span> <span class="c1"># sorted mainly to make testing the XMP file easier</span>
<span class="k">if</span> <span class="n">keyword_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">keyword_list</span><span class="p">:</span>
<span class="n">keyword_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">keyword_list</span><span class="p">)))</span> <span class="n">keyword_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">keyword_list</span><span class="p">)))</span>
<span class="k">if</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span> <span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span>
<span class="n">subject_list</span> <span class="o">=</span> <span class="n">keyword_list</span> <span class="n">subject_list</span> <span class="o">=</span> <span class="n">keyword_list</span>
<span class="k">if</span> <span class="n">location</span><span class="p">:</span>
<span class="n">latlon</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span>
<span class="n">xmp_str</span> <span class="o">=</span> <span class="n">xmp_template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span> <span class="n">xmp_str</span> <span class="o">=</span> <span class="n">xmp_template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span>
<span class="n">photo</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">photo</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span>
<span class="n">description</span><span class="o">=</span><span class="n">description</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="n">description</span><span class="p">,</span>
@@ -1843,6 +1923,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">person_list</span><span class="p">,</span> <span class="n">persons</span><span class="o">=</span><span class="n">person_list</span><span class="p">,</span>
<span class="n">subjects</span><span class="o">=</span><span class="n">subject_list</span><span class="p">,</span> <span class="n">subjects</span><span class="o">=</span><span class="n">subject_list</span><span class="p">,</span>
<span class="n">extension</span><span class="o">=</span><span class="n">extension</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="n">extension</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">latlon</span><span class="p">,</span>
<span class="n">version</span><span class="o">=</span><span class="n">__version__</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="n">__version__</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
@@ -1923,7 +2004,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_scoreinfo &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photoinfo._photoinfo_scoreinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -208,7 +208,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_searchinfo &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photoinfo._photoinfo_searchinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -366,7 +366,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -60,6 +60,7 @@
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span> <span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
<span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span> <span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span> <span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span> <span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span>
@@ -103,6 +104,7 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">info</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">info</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_verbose</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">filename</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">filename</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -484,9 +486,24 @@
<span class="p">)</span> <span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_albums</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_albums</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_albums</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_albums</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_albums</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photos is contained in &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photo is contained in &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span> <span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -496,6 +513,21 @@
<span class="p">]</span> <span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_album_info</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">album_info</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_album_info</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; ImportInfo object representing import session for the photo or None if no import session &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; ImportInfo object representing import session for the photo or None if no import session &quot;&quot;&quot;</span>
@@ -542,6 +574,30 @@
<span class="sd">&quot;&quot;&quot; True if picture has adjustments / edits &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; True if picture has adjustments / edits &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;hasAdjustments&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;hasAdjustments&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">adjustments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_adjustmentinfo</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
<span class="n">directory</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># first char of uuid</span>
<span class="n">plist_file</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">library</span><span class="p">)</span>
<span class="o">/</span> <span class="s2">&quot;resources&quot;</span>
<span class="o">/</span> <span class="s2">&quot;renders&quot;</span>
<span class="o">/</span> <span class="n">directory</span>
<span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">.plist&quot;</span>
<span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">plist_file</span><span class="o">.</span><span class="n">is_file</span><span class="p">():</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_adjustmentinfo</span> <span class="o">=</span> <span class="n">AdjustmentsInfo</span><span class="p">(</span><span class="n">plist_file</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_adjustmentinfo</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">external_edit</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">external_edit</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if picture was edited outside of Photos using external editor &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; Returns True if picture was edited outside of Photos using external editor &quot;&quot;&quot;</span>
@@ -687,6 +743,11 @@
<span class="sd">&quot;&quot;&quot; Returns True if photo is part of a Burst photo set, otherwise False &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; Returns True if photo is part of a Burst photo set, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst&quot;</span><span class="p">]</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_selected</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst_key&quot;</span><span class="p">]</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">burst_photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is a burst photo, returns list of PhotoInfo objects</span> <span class="sd">&quot;&quot;&quot;If photo is a burst photo, returns list of PhotoInfo objects</span>
@@ -855,8 +916,19 @@
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns EXIF orientation of the current photo version as int &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1184,7 +1256,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -2074,6 +2074,7 @@
<span class="c1"># &gt; 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)</span> <span class="c1"># &gt; 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;customRenderedValue&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;customRenderedValue&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;hdr&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;hdr&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;depth_state&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;portrait&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">False</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;portrait&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">False</span>
<span class="c1"># Set panorama from either KindSubType or RenderedValue</span> <span class="c1"># Set panorama from either KindSubType or RenderedValue</span>
@@ -2951,7 +2952,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -29,9 +29,14 @@ if (!window.console || !console.firebug) {
/** /**
* small helper function to urldecode strings * small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/ */
jQuery.urldecode = function(x) { jQuery.urldecode = function(x) {
return decodeURIComponent(x).replace(/\+/g, ' '); if (!x) {
return x
}
return decodeURIComponent(x.replace(/\+/g, ' '));
}; };
/** /**

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = { var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.40.4', VERSION: '0.41.6',
LANGUAGE: 'None', LANGUAGE: 'None',
COLLAPSE_INDEX: false, COLLAPSE_INDEX: false,
BUILDER: 'html', BUILDER: 'html',

View File

@@ -13,7 +13,8 @@
var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
/* Non-minified version JS is _stemmer.js if file is provided */ /* Non-minified version is copied as a separate JS file, is available */
/** /**
* Porter Stemmer * Porter Stemmer
*/ */
@@ -199,7 +200,6 @@ var Stemmer = function() {
var splitChars = (function() { var splitChars = (function() {
var result = {}; var result = {};
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,

View File

@@ -1,7 +1,7 @@
pre { line-height: 125%; } pre { line-height: 125%; }
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc } .highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; } .highlight { background: #f8f8f8; }

View File

@@ -248,7 +248,7 @@ var Search = {
// results left, load the summary and display it // results left, load the summary and display it
if (results.length) { if (results.length) {
var item = results.pop(); var item = results.pop();
var listItem = $('<li style="display:none"></li>'); var listItem = $('<li></li>');
var requestUrl = ""; var requestUrl = "";
var linkUrl = ""; var linkUrl = "";
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') { if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
@@ -273,9 +273,9 @@ var Search = {
if (item[3]) { if (item[3]) {
listItem.append($('<span> (' + item[3] + ')</span>')); listItem.append($('<span> (' + item[3] + ')</span>'));
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) { } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
$.ajax({url: requestUrl, $.ajax({url: requestUrl,
dataType: "text", dataType: "text",
@@ -285,16 +285,16 @@ var Search = {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms)); listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
} }
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
}}); }});
} else { } else {
// no source available, just display title // no source available, just display title
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} }
} }
// search finished, update title and status message // search finished, update title and status message
@@ -379,6 +379,13 @@ var Search = {
return results; return results;
}, },
/**
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
escapeRegExp : function(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
},
/** /**
* search for full-text terms in the index * search for full-text terms in the index
*/ */
@@ -402,13 +409,14 @@ var Search = {
]; ];
// add support for partial matches // add support for partial matches
if (word.length > 2) { if (word.length > 2) {
var word_regex = this.escapeRegExp(word);
for (var w in terms) { for (var w in terms) {
if (w.match(word) && !terms[word]) { if (w.match(word_regex) && !terms[word]) {
_o.push({files: terms[w], score: Scorer.partialTerm}) _o.push({files: terms[w], score: Scorer.partialTerm})
} }
} }
for (var w in titleterms) { for (var w in titleterms) {
if (w.match(word) && !titleterms[word]) { if (w.match(word_regex) && !titleterms[word]) {
_o.push({files: titleterms[w], score: Scorer.partialTitle}) _o.push({files: titleterms[w], score: Scorer.partialTitle})
} }
} }

2027
docs/_static/underscore-1.12.0.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.40.4 documentation</title> <title>Index &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -327,6 +327,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-from-date">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-from-date">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--from-time &lt;from_time&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-from-time">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-time">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -395,6 +404,15 @@
<ul> <ul>
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--in-album
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-in-album">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-in-album">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -520,8 +538,6 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li> <li>
--no-likes --no-likes
@@ -531,6 +547,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li> <li>
--no-place --no-place
@@ -590,6 +608,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--not-in-album
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-not-in-album">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-not-in-album">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -685,6 +712,13 @@
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-only-movies">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-only-movies">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-only-movies">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--only-new
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-only-new">osxphotos-export command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -751,6 +785,13 @@
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--replace-keywords
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">osxphotos-export command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -758,6 +799,13 @@
<ul> <ul>
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--retry &lt;RETRY&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">osxphotos-export command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -884,6 +932,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-to-date">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-to-date">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--to-time &lt;to_time&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-to-time">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-time">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -992,6 +1049,8 @@
<table style="width: 100%" class="indextable genindextable"><tr> <table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul> <td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.activities">activities() (osxphotos.PhotoInfo.SearchInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.activities">activities() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.adjustments">adjustments() (osxphotos.PhotoInfo property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.album_info">album_info() (osxphotos.PhotoInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.album_info">album_info() (osxphotos.PhotoInfo property)</a>
@@ -1007,10 +1066,10 @@
<li><a href="reference.html#osxphotos.PhotosDB.albums">(osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums">(osxphotos.PhotosDB property)</a>
</li> </li>
</ul></li> </ul></li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict() (osxphotos.PhotosDB property)</a>
</li>
</ul></td> </ul></td>
<td style="width: 33%; vertical-align: top;"><ul> <td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict() (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared">albums_shared() (osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums_shared">albums_shared() (osxphotos.PhotosDB property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared_as_dict">albums_shared_as_dict() (osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums_shared_as_dict">albums_shared_as_dict() (osxphotos.PhotosDB property)</a>
@@ -1037,13 +1096,19 @@
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a> <li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
</li> </li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums() (osxphotos.PhotoInfo property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_selected">burst_selected() (osxphotos.PhotoInfo property)</a>
</li> </li>
</ul></td> </ul></td>
</tr></table> </tr></table>
@@ -1436,6 +1501,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-folder">--folder &lt;FOLDER&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-folder">--folder &lt;FOLDER&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">--from-date &lt;from_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-from-date">--from-date &lt;from_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-from-time">--from-time &lt;from_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-has-comment">--has-comment</a> <li><a href="cli.html#cmdoption-osxphotos-export-has-comment">--has-comment</a>
</li> </li>
@@ -1452,6 +1519,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-date-modified">--ignore-date-modified</a> <li><a href="cli.html#cmdoption-osxphotos-export-ignore-date-modified">--ignore-date-modified</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">--ignore-signature</a> <li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">--ignore-signature</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-in-album">--in-album</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-is-reference">--is-reference</a> <li><a href="cli.html#cmdoption-osxphotos-export-is-reference">--is-reference</a>
</li> </li>
@@ -1488,6 +1557,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-not-hdr">--not-hdr</a> <li><a href="cli.html#cmdoption-osxphotos-export-not-hdr">--not-hdr</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">--not-hidden</a> <li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">--not-hidden</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-not-in-album">--not-in-album</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-not-live">--not-live</a> <li><a href="cli.html#cmdoption-osxphotos-export-not-live">--not-live</a>
</li> </li>
@@ -1506,6 +1577,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-not-time-lapse">--not-time-lapse</a> <li><a href="cli.html#cmdoption-osxphotos-export-not-time-lapse">--not-time-lapse</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">--only-movies</a> <li><a href="cli.html#cmdoption-osxphotos-export-only-movies">--only-movies</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-only-new">--only-new</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-only-photos">--only-photos</a> <li><a href="cli.html#cmdoption-osxphotos-export-only-photos">--only-photos</a>
</li> </li>
@@ -1522,8 +1595,12 @@
<li><a href="cli.html#cmdoption-osxphotos-export-place">--place &lt;PLACE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-place">--place &lt;PLACE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a> <li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report &lt;path to export report&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-report">--report &lt;path to export report&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">--retry &lt;RETRY&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a>
</li> </li>
@@ -1556,6 +1633,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-title">--title &lt;TITLE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-title">--title &lt;TITLE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">--to-date &lt;to_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-to-date">--to-date &lt;to_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-to-time">--to-time &lt;to_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-touch-file">--touch-file</a> <li><a href="cli.html#cmdoption-osxphotos-export-touch-file">--touch-file</a>
</li> </li>
@@ -1682,6 +1761,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-folder">--folder &lt;FOLDER&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-folder">--folder &lt;FOLDER&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">--from-date &lt;from_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-from-date">--from-date &lt;from_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-time">--from-time &lt;from_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-has-comment">--has-comment</a> <li><a href="cli.html#cmdoption-osxphotos-query-has-comment">--has-comment</a>
</li> </li>
@@ -1694,6 +1775,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-hidden">--hidden</a> <li><a href="cli.html#cmdoption-osxphotos-query-hidden">--hidden</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-i">--ignore-case</a> <li><a href="cli.html#cmdoption-osxphotos-query-i">--ignore-case</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-in-album">--in-album</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-incloud">--incloud</a> <li><a href="cli.html#cmdoption-osxphotos-query-incloud">--incloud</a>
</li> </li>
@@ -1728,6 +1811,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-not-hdr">--not-hdr</a> <li><a href="cli.html#cmdoption-osxphotos-query-not-hdr">--not-hdr</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">--not-hidden</a> <li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">--not-hidden</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-not-in-album">--not-in-album</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-not-incloud">--not-incloud</a> <li><a href="cli.html#cmdoption-osxphotos-query-not-incloud">--not-incloud</a>
</li> </li>
@@ -1774,6 +1859,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-title">--title &lt;TITLE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-title">--title &lt;TITLE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">--to-date &lt;to_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-to-date">--to-date &lt;to_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-time">--to-time &lt;to_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-uti">--uti &lt;UTI&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-uti">--uti &lt;UTI&gt;</a>
</li> </li>
@@ -2073,7 +2160,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.40.4 documentation</title> <title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -365,7 +365,7 @@ Alternatively, you can also run the command line utility like this: <code class=
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
| |

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.40.4 documentation</title> <title>osxphotos &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -91,7 +91,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
| |

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.40.4 documentation</title> <title>Search &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
@@ -102,7 +102,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,10 @@ github:
@make html @make html
@cp -a _build/html/. ../docs @cp -a _build/html/. ../docs
pdf:
@make latexpdf
@cp -a _build/latex/osxphotos.pdf ../docs
.PHONY: help Makefile .PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new # Catch-all target: route all unknown targets to Sphinx using the new

View File

@@ -8,7 +8,7 @@ import importlib
pathex = os.getcwd() pathex = os.getcwd()
# include necessary data files # include necessary data files
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')] datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates'), ('osxphotos/phototemplate.tx', 'osxphotos'), ('osxphotos/phototemplate.md', 'osxphotos')]
package_imports = [['photoscript', ['photoscript.applescript']]] package_imports = [['photoscript', ['photoscript.applescript']]]
for package, files in package_imports: for package, files in package_imports:
proot = os.path.dirname(importlib.import_module(package).__file__) proot = os.path.dirname(importlib.import_module(package).__file__)

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.40.7" __version__ = "0.41.7"

View File

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

View File

@@ -131,6 +131,21 @@ class DateTimeISO8601(click.ParamType):
) )
class TimeISO8601(click.ParamType):
name = "TIME"
def convert(self, value, param, ctx):
try:
return datetime.time.fromisoformat(value).replace(tzinfo=None)
except Exception:
self.fail(
f"Invalid value for --{param.name}: invalid time format {value}. "
"Valid format: HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] "
"however, note that timezone will be ignored."
)
# Click CLI object & context settings # Click CLI object & context settings
class CLI_Obj: class CLI_Obj:
def __init__(self, db=None, json=False, debug=False): def __init__(self, db=None, json=False, debug=False):
@@ -236,7 +251,7 @@ def query_options(f):
default=None, default=None,
multiple=False, multiple=False,
help="Search for photos with UUID(s) loaded from FILE. " help="Search for photos with UUID(s) loaded from FILE. "
"Format is a single UUID per line. Lines preceeded with # are ignored.", "Format is a single UUID per line. Lines preceded with # are ignored.",
type=click.Path(exists=True), type=click.Path(exists=True),
), ),
o( o(
@@ -386,14 +401,24 @@ def query_options(f):
), ),
o( o(
"--from-date", "--from-date",
help="Search by start item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(), type=DateTimeISO8601(),
), ),
o( o(
"--to-date", "--to-date",
help="Search by end item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(), type=DateTimeISO8601(),
), ),
o(
"--from-time",
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
type=TimeISO8601(),
),
o(
"--to-time",
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
type=TimeISO8601(),
),
o("--has-comment", is_flag=True, help="Search for photos that have comments."), o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."), o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."), o("--has-likes", is_flag=True, help="Search for photos that have likes."),
@@ -403,6 +428,16 @@ def query_options(f):
is_flag=True, is_flag=True,
help="Search for photos that were imported as referenced files (not copied into Photos library).", help="Search for photos that were imported as referenced files (not copied into Photos library).",
), ),
o(
"--in-album",
is_flag=True,
help="Search for photos that are in one or more albums.",
),
o(
"--not-in-album",
is_flag=True,
help="Search for photos that are not in any albums.",
),
] ]
for o in options[::-1]: for o in options[::-1]:
f = o(f) f = o(f)
@@ -437,11 +472,18 @@ def cli(ctx, db, json_, debug):
@click.option( @click.option(
"--ignore-signature", "--ignore-signature",
is_flag=True, is_flag=True,
help="When used with --update, ignores file signature when updating files. " help="When used with '--update', ignores file signature when updating files. "
"This is useful if you have processed or edited exported photos changing the " "This is useful if you have processed or edited exported photos changing the "
"file signature (size & modification date). In this case, --update would normally " "file signature (size & modification date). In this case, '--update' would normally "
"re-export the processed files but with --ignore-signature, files which exist " "re-export the processed files but with '--ignore-signature', files which exist "
"in the export directory will not be re-exported.", "in the export directory will not be re-exported. "
"If used with '--sidecar', '--ignore-signature' has the following behavior: "
"1) if the metadata (in Photos) that went into the sidecar did not change, "
"the sidecar will not be updated; "
"2) if the metadata (in Photos) that went into the sidecar did change, "
"a new sidecar is written but a new image file is not; "
"3) if a sidecar does not exist for the photo, a sidecar will be written "
"whether or not the photo file was written or updated.",
) )
@click.option( @click.option(
"--only-new", "--only-new",
@@ -475,6 +517,13 @@ def cli(ctx, db, json_, debug):
"Use this with caution as it may create name collisions on export. " "Use this with caution as it may create name collisions on export. "
"(e.g. if two files happen to have the same name)", "(e.g. if two files happen to have the same name)",
) )
@click.option(
"--retry",
metavar="RETRY",
type=click.INT,
help="Automatically retry export up to RETRY times if an error occurs during export. "
"This may be useful with network drives that experience intermittent errors.",
)
@click.option( @click.option(
"--export-by-date", "--export-by-date",
is_flag=True, is_flag=True,
@@ -559,7 +608,8 @@ def cli(ctx, db, json_, debug):
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. " "\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
"Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. " "Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. "
"Sidecar filename is in format photoname.ext.json; " "Sidecar filename is in format photoname.ext.json; "
"For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.", "For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'. "
"See also '--ignore-signature'.",
) )
@click.option( @click.option(
"--sidecar-drop-ext", "--sidecar-drop-ext",
@@ -642,8 +692,16 @@ def cli(ctx, db, json_, debug):
"the full path to the folder and album photo is contained in as a keyword when exporting " "the full path to the folder and album photo is contained in as a keyword when exporting "
'you could specify --keyword-template "{folder_album}" ' 'you could specify --keyword-template "{folder_album}" '
'You may specify more than one template, for example --keyword-template "{folder_album}" ' 'You may specify more than one template, for example --keyword-template "{folder_album}" '
'--keyword-template "{created.year}" ' '--keyword-template "{created.year}". '
"See Templating System below.", "See '--replace-keywords' and Templating System below.",
)
@click.option(
"--replace-keywords",
is_flag=True,
help="Replace keywords with any values specified with --keyword-template. "
"By default, --keyword-template will add keywords to any keywords already associated "
"with the photo. If --replace-keywords is specified, values from --keyword-template "
"will replace any existing keywords instead of adding additional keywords.",
) )
@click.option( @click.option(
"--description-template", "--description-template",
@@ -756,7 +814,10 @@ def cli(ctx, db, json_, debug):
"--cleanup", "--cleanup",
is_flag=True, is_flag=True,
help="Cleanup export directory by deleting any files which were not included in this export set. " help="Cleanup export directory by deleting any files which were not included in this export set. "
"For example, photos which had previously been exported and were subsequently deleted in Photos.", "For example, photos which had previously been exported and were subsequently deleted in Photos. "
"WARNING: --cleanup will delete *any* files in the export directory that were not exported by osxphotos, "
"for example, your own scripts or other files. Be sure this is what you intend before using "
"--cleanup. Use --dry-run with --cleanup first if you're not certain.",
) )
@click.option( @click.option(
"--exportdb", "--exportdb",
@@ -827,6 +888,8 @@ def export(
not_shared, not_shared,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
verbose, verbose,
missing, missing,
update, update,
@@ -836,6 +899,7 @@ def export(
export_as_hardlink, export_as_hardlink,
touch_file, touch_file,
overwrite, overwrite,
retry,
export_by_date, export_by_date,
skip_edited, skip_edited,
skip_original_if_edited, skip_original_if_edited,
@@ -845,6 +909,7 @@ def export(
person_keyword, person_keyword,
album_keyword, album_keyword,
keyword_template, keyword_template,
replace_keywords,
description_template, description_template,
finder_tag_template, finder_tag_template,
finder_tag_keywords, finder_tag_keywords,
@@ -907,6 +972,8 @@ def export(
save_config, save_config,
is_reference, is_reference,
beta, beta,
in_album,
not_in_album,
): ):
"""Export photos from the Photos database. """Export photos from the Photos database.
Export path DEST is required. Export path DEST is required.
@@ -971,6 +1038,8 @@ def export(
not_shared = cfg.not_shared not_shared = cfg.not_shared
from_date = cfg.from_date from_date = cfg.from_date
to_date = cfg.to_date to_date = cfg.to_date
from_time = cfg.from_time
to_time = cfg.to_time
verbose = cfg.verbose verbose = cfg.verbose
missing = cfg.missing missing = cfg.missing
update = cfg.update update = cfg.update
@@ -979,6 +1048,7 @@ def export(
export_as_hardlink = cfg.export_as_hardlink export_as_hardlink = cfg.export_as_hardlink
touch_file = cfg.touch_file touch_file = cfg.touch_file
overwrite = cfg.overwrite overwrite = cfg.overwrite
retry = cfg.retry
export_by_date = cfg.export_by_date export_by_date = cfg.export_by_date
skip_edited = cfg.skip_edited skip_edited = cfg.skip_edited
skip_original_if_edited = cfg.skip_original_if_edited skip_original_if_edited = cfg.skip_original_if_edited
@@ -988,6 +1058,7 @@ def export(
person_keyword = cfg.person_keyword person_keyword = cfg.person_keyword
album_keyword = cfg.album_keyword album_keyword = cfg.album_keyword
keyword_template = cfg.keyword_template keyword_template = cfg.keyword_template
replace_keywords = cfg.replace_keywords
description_template = cfg.description_template description_template = cfg.description_template
finder_tag_template = cfg.finder_tag_template finder_tag_template = cfg.finder_tag_template
finder_tag_keywords = cfg.finder_tag_keywords finder_tag_keywords = cfg.finder_tag_keywords
@@ -1047,6 +1118,8 @@ def export(
exportdb = cfg.exportdb exportdb = cfg.exportdb
beta = cfg.beta beta = cfg.beta
only_new = cfg.only_new only_new = cfg.only_new
in_album = cfg.in_album
not_in_album = cfg.not_in_album
# config file might have changed verbose # config file might have changed verbose
VERBOSE = bool(verbose) VERBOSE = bool(verbose)
@@ -1080,6 +1153,7 @@ def export(
("shared", "not_shared"), ("shared", "not_shared"),
("has_comment", "no_comment"), ("has_comment", "no_comment"),
("has_likes", "no_likes"), ("has_likes", "no_likes"),
("in_album", "not_in_album"),
] ]
dependent_options = [ dependent_options = [
("missing", ("download_missing", "use_photos_export")), ("missing", ("download_missing", "use_photos_export")),
@@ -1134,6 +1208,7 @@ def export(
original_suffix = ( original_suffix = (
DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix
) )
retry = 0 if not retry else retry
if not os.path.isdir(dest): if not os.path.isdir(dest):
click.echo( click.echo(
@@ -1314,6 +1389,8 @@ def export(
not_incloud=False, not_incloud=False,
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait, portrait=portrait,
not_portrait=not_portrait, not_portrait=not_portrait,
screenshot=screenshot, screenshot=screenshot,
@@ -1339,6 +1416,11 @@ def export(
has_likes=has_likes, has_likes=has_likes,
no_likes=no_likes, no_likes=no_likes,
is_reference=is_reference, is_reference=is_reference,
in_album=in_album,
not_in_album=not_in_album,
burst_photos=export_bursts,
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
missing_bursts=(download_missing and use_photokit) or not download_missing,
) )
if photos: if photos:
@@ -1347,13 +1429,6 @@ def export(
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()} previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
photos = [p for p in photos if p.uuid not in previous_uuids] photos = [p for p in photos if p.uuid not in previous_uuids]
if export_bursts:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
burst_set = [p for p in burst.burst_photos if not p.ismissing]
photos.extend(burst_set)
num_photos = len(photos) num_photos = len(photos)
# TODO: photos or photo appears several times, pull into a separate function # TODO: photos or photo appears several times, pull into a separate function
photo_str = "photos" if num_photos > 1 else "photo" photo_str = "photos" if num_photos > 1 else "photo"
@@ -1409,6 +1484,8 @@ def export(
exiftool_option=exiftool_option, exiftool_option=exiftool_option,
strip=strip, strip=strip,
jpeg_ext=jpeg_ext, jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
) )
results += export_results results += export_results
@@ -1470,10 +1547,14 @@ def export(
+ [str(pathlib.Path(export_db_path).resolve())] + [str(pathlib.Path(export_db_path).resolve())]
) )
click.echo(f"Cleaning up {dest}") click.echo(f"Cleaning up {dest}")
(cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil) cleaned_files, cleaned_dirs = cleanup_files(dest, all_files, fileutil)
file_str = "files" if cleaned_files != 1 else "file" file_str = "files" if len(cleaned_files) != 1 else "file"
dir_str = "directories" if cleaned_dirs != 1 else "directory" dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}") click.echo(
f"Deleted: {len(cleaned_files)} {file_str}, {len(cleaned_dirs)} {dir_str}"
)
results.deleted_files = cleaned_files
results.deleted_directories = cleaned_dirs
if report: if report:
verbose_(f"Writing export report to {report}") verbose_(f"Writing export report to {report}")
@@ -1595,6 +1676,8 @@ def query(
not_incloud, not_incloud,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
portrait, portrait,
not_portrait, not_portrait,
screenshot, screenshot,
@@ -1620,6 +1703,8 @@ def query(
has_likes, has_likes,
no_likes, no_likes,
is_reference, is_reference,
in_album,
not_in_album,
): ):
"""Query the Photos database using 1 or more search options; """Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND" if more than one option is provided, they are treated as "AND"
@@ -1641,6 +1726,8 @@ def query(
has_raw, has_raw,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
label, label,
is_reference, is_reference,
] ]
@@ -1667,6 +1754,7 @@ def query(
(shared, not_shared), (shared, not_shared),
(has_comment, no_comment), (has_comment, no_comment),
(has_likes, no_likes), (has_likes, no_likes),
(in_album, not_in_album),
] ]
# print help if no non-exclusive term or a double exclusive term is given # print help if no non-exclusive term or a double exclusive term is given
if any(all(bb) for bb in exclusive) or not any( if any(all(bb) for bb in exclusive) or not any(
@@ -1734,6 +1822,8 @@ def query(
not_incloud=not_incloud, not_incloud=not_incloud,
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait, portrait=portrait,
not_portrait=not_portrait, not_portrait=not_portrait,
screenshot=screenshot, screenshot=screenshot,
@@ -1759,6 +1849,8 @@ def query(
has_likes=has_likes, has_likes=has_likes,
no_likes=no_likes, no_likes=no_likes,
is_reference=is_reference, is_reference=is_reference,
in_album=in_album,
not_in_album=not_in_album,
) )
# below needed for to make CliRunner work for testing # below needed for to make CliRunner work for testing
@@ -1905,6 +1997,8 @@ def _query(
not_incloud=None, not_incloud=None,
from_date=None, from_date=None,
to_date=None, to_date=None,
from_time=None,
to_time=None,
portrait=None, portrait=None,
not_portrait=None, not_portrait=None,
screenshot=None, screenshot=None,
@@ -1930,6 +2024,10 @@ def _query(
has_likes=False, has_likes=False,
no_likes=False, no_likes=False,
is_reference=False, is_reference=False,
in_album=False,
not_in_album=False,
burst_photos=None,
missing_bursts=None,
): ):
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands """Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
@@ -2162,6 +2260,38 @@ def _query(
if is_reference: if is_reference:
photos = [p for p in photos if p.isreference] photos = [p for p in photos if p.isreference]
if in_album:
photos = [p for p in photos if p.albums]
elif not_in_album:
photos = [p for p in photos if not p.albums]
if from_time:
photos = [p for p in photos if p.date.time() >= from_time]
if to_time:
photos = [p for p in photos if p.date.time() <= to_time]
if burst_photos:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
if missing_bursts:
# include burst photos that are missing
photos.extend(burst.burst_photos)
else:
# don't include missing burst images (these can't be downloaded with AppleScript)
photos.extend([p for p in burst.burst_photos if not p.ismissing])
# remove duplicates as each burst photo in the set that's selected would
# result in the entire set being added above
# can't use set() because PhotoInfo not hashable
seen_uuids = {}
for p in photos:
if p.uuid in seen_uuids:
continue
seen_uuids[p.uuid] = p
photos = list(seen_uuids.values())
return photos return photos
@@ -2233,6 +2363,8 @@ def export_photo(
exiftool_option=None, exiftool_option=None,
strip=False, strip=False,
jpeg_ext=None, jpeg_ext=None,
replace_keywords=False,
retry=0,
): ):
"""Helper function for export that does the actual export """Helper function for export that does the actual export
@@ -2271,6 +2403,8 @@ def export_photo(
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
jpeg_ext: if not None, specify the extension to use for all JPEG images on export jpeg_ext: if not None, specify the extension to use for all JPEG images on export
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
retry: retry up to retry # of times if there's an error
Returns: Returns:
list of path(s) of exported photo or None if photo was missing list of path(s) of exported photo or None if photo was missing
@@ -2334,15 +2468,15 @@ def export_photo(
rendered_suffix, unmatched = photo.render_template( rendered_suffix, unmatched = photo.render_template(
original_suffix, filename=True, strip=strip original_suffix, filename=True, strip=strip
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"original_suffix", "original_suffix",
f"Invalid template for --original-suffix '{original_suffix}'", f"Invalid template for --original-suffix '{original_suffix}': {e}",
) )
if not rendered_suffix or unmatched: if not rendered_suffix or unmatched:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"original_suffix", "original_suffix",
f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unmatched={unmatched}", f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unknown field={unmatched}",
) )
if len(rendered_suffix) > 1: if len(rendered_suffix) > 1:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -2352,13 +2486,16 @@ def export_photo(
rendered_suffix = rendered_suffix[0] rendered_suffix = rendered_suffix[0]
original_filename = pathlib.Path(filename) original_filename = pathlib.Path(filename)
file_ext = ( file_ext = original_filename.suffix
"." + jpeg_ext if photo.isphoto and (jpeg_ext or convert_to_jpeg):
if jpeg_ext and (photo.uti == "public.jpeg" or convert_to_jpeg) # change the file extension to correct jpeg extension if needed
else ".jpeg" file_ext = (
if convert_to_jpeg and photo.uti != "public.jpeg" "." + jpeg_ext
else original_filename.suffix if jpeg_ext and (photo.uti_original == "public.jpeg" or convert_to_jpeg)
) else ".jpeg"
if convert_to_jpeg and photo.uti_original != "public.jpeg"
else original_filename.suffix
)
original_filename = ( original_filename = (
original_filename.parent original_filename.parent
/ f"{original_filename.stem}{rendered_suffix}{file_ext}" / f"{original_filename.stem}{rendered_suffix}{file_ext}"
@@ -2417,71 +2554,88 @@ def export_photo(
str(pathlib.Path(dest_path) / original_filename) str(pathlib.Path(dest_path) / original_filename)
) )
else: else:
try: tries = 0
export_results = photo.export2( while tries <= retry:
dest_path, tries += 1
original_filename, error = 0
sidecar=sidecar_flags, try:
sidecar_drop_ext=sidecar_drop_ext, export_results = photo.export2(
live_photo=export_live, dest_path,
raw_photo=export_raw, original_filename,
export_as_hardlink=export_as_hardlink, sidecar=sidecar_flags,
overwrite=overwrite, sidecar_drop_ext=sidecar_drop_ext,
use_photos_export=use_photos_export, live_photo=export_live,
exiftool=exiftool, raw_photo=export_raw,
merge_exif_keywords=exiftool_merge_keywords, export_as_hardlink=export_as_hardlink,
merge_exif_persons=exiftool_merge_persons, overwrite=overwrite,
use_albums_as_keywords=album_keyword, use_photos_export=use_photos_export,
use_persons_as_keywords=person_keyword, exiftool=exiftool,
keyword_template=keyword_template, merge_exif_keywords=exiftool_merge_keywords,
description_template=description_template, merge_exif_persons=exiftool_merge_persons,
update=update, use_albums_as_keywords=album_keyword,
ignore_signature=ignore_signature, use_persons_as_keywords=person_keyword,
export_db=export_db, keyword_template=keyword_template,
fileutil=fileutil, description_template=description_template,
dry_run=dry_run, update=update,
touch_file=touch_file, ignore_signature=ignore_signature,
convert_to_jpeg=convert_to_jpeg, export_db=export_db,
jpeg_quality=jpeg_quality, fileutil=fileutil,
ignore_date_modified=ignore_date_modified, dry_run=dry_run,
use_photokit=use_photokit, touch_file=touch_file,
verbose=verbose_, convert_to_jpeg=convert_to_jpeg,
exiftool_flags=exiftool_option, jpeg_quality=jpeg_quality,
jpeg_ext=jpeg_ext, ignore_date_modified=ignore_date_modified,
) use_photokit=use_photokit,
results += export_results verbose=verbose_,
for warning_ in export_results.exiftool_warning: exiftool_flags=exiftool_option,
verbose_( jpeg_ext=jpeg_ext,
f"exiftool warning for file {warning_[0]}: {warning_[1]}" replace_keywords=replace_keywords,
) )
for error_ in export_results.exiftool_error: for warning_ in export_results.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo( click.echo(
click.style( click.style(
f"exiftool error for file {error_[0]}: {error_[1]}", f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR, fg=CLI_COLOR_ERROR,
), ),
err=True, err=True,
) )
for error_ in export_results.error: if tries > retry:
click.echo( results.error.append(
click.style( (str(pathlib.Path(dest) / original_filename), e)
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", )
fg=CLI_COLOR_ERROR, break
), else:
err=True, click.echo(
) f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / original_filename), e)
)
else: else:
verbose_(f"Skipping original version of {photo.original_filename}") verbose_(f"Skipping original version of {photo.original_filename}")
@@ -2490,17 +2644,28 @@ def export_photo(
if export_edited and photo.hasadjustments: if export_edited and photo.hasadjustments:
edited_filename = pathlib.Path(filename) edited_filename = pathlib.Path(filename)
edited_ext = ( edited_ext = (
"." + jpeg_ext # rare cases on Photos <= 4 that uti_edited is None
if jpeg_ext and photo.uti_edited == "public.jpeg" "." + get_preferred_uti_extension(photo.uti_edited)
else "." + get_preferred_uti_extension(photo.uti_edited)
if photo.uti_edited if photo.uti_edited
else pathlib.Path(photo.path_edited).suffix else pathlib.Path(photo.path_edited).suffix
if photo.path_edited if photo.path_edited
else pathlib.Path(photo.filename).suffix else pathlib.Path(photo.filename).suffix
) )
if (
photo.isphoto
and jpeg_ext
and edited_ext.lower() in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext
# Big Sur uses .heic for some edited photos so need to check # Big Sur uses .heic for some edited photos so need to check
# if extension isn't jpeg/jpg and using --convert-to-jpeg # if extension isn't jpeg/jpg and using --convert-to-jpeg
if convert_to_jpeg and edited_ext.lower() not in [".jpg", ".jpeg"]: if (
photo.isphoto
and convert_to_jpeg
and edited_ext.lower() not in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg" edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
if edited_suffix: if edited_suffix:
@@ -2508,15 +2673,15 @@ def export_photo(
rendered_suffix, unmatched = photo.render_template( rendered_suffix, unmatched = photo.render_template(
edited_suffix, filename=True, strip=strip edited_suffix, filename=True, strip=strip
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"edited_suffix", "edited_suffix",
f"Invalid template for --edited-suffix '{edited_suffix}'", f"Invalid template for --edited-suffix '{edited_suffix}': {e}",
) )
if not rendered_suffix or unmatched: if not rendered_suffix or unmatched:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"edited_suffix", "edited_suffix",
f"Invalid template for --edited-suffix '{edited_suffix}': results={rendered_suffix} unmatched={unmatched}", f"Invalid template for --edited-suffix '{edited_suffix}': unknown field={unmatched}",
) )
if len(rendered_suffix) > 1: if len(rendered_suffix) > 1:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -2554,69 +2719,87 @@ def export_photo(
) )
else: else:
try: tries = 0
export_results_edited = photo.export2( while tries <= retry:
dest_path, tries += 1
edited_filename, error = 0
sidecar=sidecar_flags, try:
sidecar_drop_ext=sidecar_drop_ext, export_results_edited = photo.export2(
export_as_hardlink=export_as_hardlink, dest_path,
overwrite=overwrite, edited_filename,
edited=True, sidecar=sidecar_flags,
use_photos_export=use_photos_export, sidecar_drop_ext=sidecar_drop_ext,
exiftool=exiftool, export_as_hardlink=export_as_hardlink,
merge_exif_keywords=exiftool_merge_keywords, overwrite=overwrite,
merge_exif_persons=exiftool_merge_persons, edited=True,
use_albums_as_keywords=album_keyword, use_photos_export=use_photos_export,
use_persons_as_keywords=person_keyword, exiftool=exiftool,
keyword_template=keyword_template, merge_exif_keywords=exiftool_merge_keywords,
description_template=description_template, merge_exif_persons=exiftool_merge_persons,
update=update, use_albums_as_keywords=album_keyword,
ignore_signature=ignore_signature, use_persons_as_keywords=person_keyword,
export_db=export_db, keyword_template=keyword_template,
fileutil=fileutil, description_template=description_template,
dry_run=dry_run, update=update,
touch_file=touch_file, ignore_signature=ignore_signature,
convert_to_jpeg=convert_to_jpeg, export_db=export_db,
jpeg_quality=jpeg_quality, fileutil=fileutil,
ignore_date_modified=ignore_date_modified, dry_run=dry_run,
use_photokit=use_photokit, touch_file=touch_file,
verbose=verbose_, convert_to_jpeg=convert_to_jpeg,
exiftool_flags=exiftool_option, jpeg_quality=jpeg_quality,
jpeg_ext=jpeg_ext, ignore_date_modified=ignore_date_modified,
) use_photokit=use_photokit,
results += export_results_edited verbose=verbose_,
for warning_ in export_results_edited.exiftool_warning: exiftool_flags=exiftool_option,
verbose_( jpeg_ext=jpeg_ext,
f"exiftool warning for file {warning_[0]}: {warning_[1]}" replace_keywords=replace_keywords,
) )
for error_ in export_results_edited.exiftool_error: for warning_ in export_results_edited.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results_edited.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results_edited.error:
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results_edited
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo( click.echo(
click.style( click.style(
f"exiftool error for file {error_[0]}: {error_[1]}", f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR, fg=CLI_COLOR_ERROR,
), ),
err=True, err=True,
) )
for error_ in export_results_edited.error: if tries > retry:
click.echo( results.error.append(
click.style( (str(pathlib.Path(dest) / edited_filename), e)
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", )
fg=CLI_COLOR_ERROR, break
), else:
err=True, click.echo(
) f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
except Exception as e: )
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / edited_filename), e)
)
if verbose: if verbose:
if update: if update:
@@ -2655,14 +2838,14 @@ def get_filenames_from_template(photo, filename_template, original_name, strip=F
filenames, unmatched = photo.render_template( filenames, unmatched = photo.render_template(
filename_template, path_sep="_", filename=True, strip=strip filename_template, path_sep="_", filename=True, strip=strip
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"filename_template", f"Invalid template '{filename_template}'" "filename_template", f"Invalid template '{filename_template}': {e}"
) )
if not filenames or unmatched: if not filenames or unmatched:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"filename_template", "filename_template",
f"Invalid template '{filename_template}': results={filenames} unmatched={unmatched}", f"Invalid template '{filename_template}': unknown field={unmatched}",
) )
filenames = [f"{file_}{photo_ext}" for file_ in filenames] filenames = [f"{file_}{photo_ext}" for file_ in filenames]
else: else:
@@ -2709,12 +2892,14 @@ def get_dirnames_from_template(
dirnames, unmatched = photo.render_template( dirnames, unmatched = photo.render_template(
directory, dirname=True, strip=strip directory, dirname=True, strip=strip
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'") raise click.BadOptionUsage(
"directory", f"Invalid template '{directory}': {e}"
)
if not dirnames or unmatched: if not dirnames or unmatched:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"directory", "directory",
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}", f"Invalid template '{directory}': unknown field={unmatched}",
) )
dest_paths = [] dest_paths = []
@@ -2825,8 +3010,12 @@ def write_export_report(report_file, results):
"exiftool_error": "", "exiftool_error": "",
"extended_attributes_written": 0, "extended_attributes_written": 0,
"extended_attributes_skipped": 0, "extended_attributes_skipped": 0,
"cleanup_deleted_file": 0,
"cleanup_deleted_directory": 0,
} }
for result in results.all_files() for result in results.all_files()
+ results.deleted_files
+ results.deleted_directories
} }
for result in results.exported: for result in results.exported:
@@ -2892,6 +3081,12 @@ def write_export_report(report_file, results):
for result in results.xattr_skipped: for result in results.xattr_skipped:
all_results[result]["extended_attributes_skipped"] = 1 all_results[result]["extended_attributes_skipped"] = 1
for result in results.deleted_files:
all_results[result]["cleanup_deleted_file"] = 1
for result in results.deleted_directories:
all_results[result]["cleanup_deleted_directory"] = 1
report_columns = [ report_columns = [
"filename", "filename",
"exported", "exported",
@@ -2910,6 +3105,8 @@ def write_export_report(report_file, results):
"exiftool_error", "exiftool_error",
"extended_attributes_written", "extended_attributes_written",
"extended_attributes_skipped", "extended_attributes_skipped",
"cleanup_deleted_file",
"cleanup_deleted_directory",
] ]
try: try:
@@ -2936,27 +3133,27 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
fileutile: FileUtil object fileutile: FileUtil object
Returns: Returns:
tuple of (number of files deleted, number of directories deleted) tuple of (list of files deleted, list of directories deleted)
""" """
keepers = {filename.lower(): 1 for filename in files_to_keep} keepers = {str(filename).lower(): 1 for filename in files_to_keep}
deleted_files = 0 deleted_files = []
for p in pathlib.Path(dest_path).rglob("*"): for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower() path = str(p).lower()
if p.is_file() and path not in keepers: if p.is_file() and path not in keepers:
verbose_(f"Deleting {p}") verbose_(f"Deleting {p}")
fileutil.unlink(p) fileutil.unlink(p)
deleted_files += 1 deleted_files.append(str(p))
# delete empty directories # delete empty directories
deleted_dirs = 0 deleted_dirs = []
for p in pathlib.Path(dest_path).rglob("*"): for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower() path = str(p).lower()
# if directory and directory is empty # if directory and directory is empty
if p.is_dir() and not next(p.iterdir(), False): if p.is_dir() and not next(p.iterdir(), False):
verbose_(f"Deleting empty directory {p}") verbose_(f"Deleting empty directory {p}")
fileutil.rmdir(p) fileutil.rmdir(p)
deleted_dirs += 1 deleted_dirs.append(str(p))
return (deleted_files, deleted_dirs) return (deleted_files, deleted_dirs)
@@ -3015,16 +3212,16 @@ def write_finder_tags(
path_sep="/", path_sep="/",
strip=strip, strip=strip,
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"finder_tag_template", "finder_tag_template",
f"Invalid template for --finder-tag-template': {template_str}", f"Invalid template for --finder-tag-template '{template_str}': {e}",
) )
if unmatched: if unmatched:
click.echo( click.echo(
click.style( click.style(
f"Warning: unmatched template substitution for template: {template_str} {unmatched}", f"Warning: unknown field for template: {template_str} unknown field = {unmatched}",
fg=CLI_COLOR_WARNING, fg=CLI_COLOR_WARNING,
), ),
err=True, err=True,
@@ -3071,15 +3268,15 @@ def write_extended_attributes(photo, files, xattr_template, strip=False):
path_sep="/", path_sep="/",
strip=strip, strip=strip,
) )
except ValueError: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
"xattr_template", "xattr_template",
f"Invalid template for --xattr-template': {template_str}", f"Invalid template for --xattr-template '{template_str}': {e}",
) )
if unmatched: if unmatched:
click.echo( click.echo(
click.style( click.style(
f"Warning: unmatched template substitution for template: {template_str} {unmatched}", f"Warning: unmatched template substitution for template: {template_str} unknown field={unmatched}",
fg=CLI_COLOR_WARNING, fg=CLI_COLOR_WARNING,
), ),
err=True, err=True,

View File

@@ -1,14 +1,23 @@
"""Help text helper class for osxphotos CLI """ """Help text helper class for osxphotos CLI """
import io
import re
import click import click
import osxmetadata import osxmetadata
from rich.console import Console
from rich.markdown import Markdown
from ._constants import ( from ._constants import (
EXTENDED_ATTRIBUTE_NAMES, EXTENDED_ATTRIBUTE_NAMES,
EXTENDED_ATTRIBUTE_NAMES_QUOTED, EXTENDED_ATTRIBUTE_NAMES_QUOTED,
OSXPHOTOS_EXPORT_DB, OSXPHOTOS_EXPORT_DB,
) )
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED from .phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
get_template_help,
)
class ExportCommand(click.Command): class ExportCommand(click.Command):
@@ -17,11 +26,11 @@ class ExportCommand(click.Command):
def get_help(self, ctx): def get_help(self, ctx):
help_text = super().get_help(ctx) help_text = super().get_help(ctx)
formatter = click.HelpFormatter() formatter = click.HelpFormatter()
# passed to click.HelpFormatter.write_dl for formatting # passed to click.HelpFormatter.write_dl for formatting
formatter.write("\n\n") formatter.write("\n\n")
formatter.write_text("** Export **") formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
formatter.write("\n")
formatter.write_text( formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level " "When exporting photos, osxphotos creates a database in the top-level "
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information " + f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
@@ -56,7 +65,7 @@ class ExportCommand(click.Command):
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database." + f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
) )
formatter.write("\n\n") formatter.write("\n\n")
formatter.write_text("** Extended Attributes **") formatter.write(rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width))
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
""" """
@@ -90,124 +99,9 @@ The following attributes may be used with '--xattr-template':
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" "For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
) )
formatter.write("\n\n") formatter.write("\n\n")
formatter.write_text("** Templating System **") formatter.write(rich_text("[bold]** Templating System **[/bold]", width=formatter.width))
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write(template_help(width=formatter.width))
"""
Several options, such as --directory, allow you to specify a template which
will be rendered to substitute template fields with values from the photo.
For example, '{created.month}' would be replaced with the month name of the
photo creation date. e.g. 'November'.
Some options supporting templates may be repeated e.g., --keyword-template
'{label}' --keyword-template '{media_type}' to add both labels and media
types to the keywords.
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is:
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD
is optional.
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place'
with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'.
For example, a photo with keywords 'foo' and 'bar':
'{keyword}' renders to 'foo' and 'bar'
'{,+keyword}' renders to: 'foo,bar'
'{; +keyword}' renders to: 'foo; bar'
'{+keyword}' renders to 'foobar'
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in
that they join multiple elements into a single path-like string. For example,
if photo is in album Album1 in folder Folder1, '{folder_album}' results in
'Folder1/Album1'. This is so these template fields may be used as paths in
--directory. If you intend to use such a field as a string, e.g. in the
filename, you may specify a different path separator using the form:
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
would result in 'Folder1Album1'.
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value
with text "NEW". For example, if you have album names with '/' in the album name you
could replace '/' with "-" using the template '{album[/,-]}'. This would replace
any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus
become "Vacation-2019". You may specify more than one pair of OLD,NEW values by
listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both
'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by
omitting the NEW value as in '{album[/,]}'.
- '?' Some template fields such as 'hdr' are boolean and resolve to True or False.
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image
and 'not_hdr' otherwise.
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results
in a null (empty) value, the template will result in default value of '_'.
You may specify an alternate default value by appending ',DEFAULT' after
template_field. Example: '{title,no_title}' would result in 'no_title' if the photo
had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was
no address associated with the photo, the resulting output would be:
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
contain a brace symbol ('{' or '}').
Again, 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.
You may specify a null default (e.g. "" or empty string) by omitting the value
after the comma, e.g. {title,} which would render to "" if title had no value thus
effectively deleting the template from the resulting string.
You may include other text in the template string outside the {}
and use more than one template field in a single string,
e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
Some templates may resolve to more than one value. For example, a photo can
have multiple keywords so '{keyword}' can result in multiple values. If used
in a filename or directory, these templates may result in more than one copy
of the photo being exported. For example, if photo has keywords "foo" and
"bar", --directory '{keyword}' will result in copies of the photo being
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
customization of the output. For example, '{media_type}' resolves to the
special media type of the photo such as 'panorama' or 'selfie'. You may use
the 'DEFAULT' value to override these in form:
'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if
photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée'
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
ordinary video.
With the --directory and --filename options you may specify a template for the
export directory or filename, respectively. The directory will be appended to
the export path specified in the export DEST argument to export. For example,
if template is '{created.year}/{created.month}', and export destination DEST
is '/Users/maria/Pictures/export', the actual export directory for a photo
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
March 2020.
The templating system may also be used with the --keyword-template option to
set keywords on export (with --exiftool or --sidecar), for example, to set a
new keyword in format 'folder/subfolder/album' to preserve the folder/album
structure, you can use --keyword-template "{folder_album}"
In the template, valid template substitutions will be replaced by the
corresponding value from the table below. Invalid substitutions will result
in an error.
If you want the actual text of the template substition to appear in the
rendered name, use double braces, e.g. '{{' or '}}', thus using
'{created.year}/{{name}}' for --directory would result in output of
2020/{name}/photoname.jpg
"""
)
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
"With the --directory and --filename options you may specify a template for the " "With the --directory and --filename options you may specify a template for the "
@@ -224,7 +118,8 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
"The templating system may also be used with the --keyword-template option " "The templating system may also be used with the --keyword-template option "
+ "to set keywords on export (with --exiftool or --sidecar), " + "to set keywords on export (with --exiftool or --sidecar), "
+ "for example, to set a new keyword in format 'folder/subfolder/album' to " + "for example, to set a new keyword in format 'folder/subfolder/album' to "
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"' + 'preserve the folder/album structure, you can use --keyword-template "{folder_album}" '
+ "or in the 'folder>subfolder>album' format used in Lightroom Classic, --keyword-template \"{folder_album(>)}\"."
) )
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
@@ -233,33 +128,7 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
+ "an error and the script will abort." + "an error and the script will abort."
) )
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write(rich_text("[bold]** Template Substitutions **[/bold]", width=formatter.width))
"If you want the actual text of the template substition to appear "
+ "in the rendered name, use double braces, e.g. '{{' or '}}', thus "
+ "using '{created.year}/{{name}}' for --directory "
+ "would result in output of 2020/{name}/photoname.jpg"
)
formatter.write("\n")
formatter.write_text(
"You may specify an optional default value to use if the substitution does not contain a value "
+ "(e.g. the value is null) "
+ "by specifying the default value after a ',' in the template string: "
+ "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' "
+ "but there was no address associated with the photo, the resulting output would be: "
+ "'2020/NO_ADDRESS/photoname.jpg'. "
+ "If specified, the default value may not contain a brace symbol ('{' or '}')."
)
formatter.write("\n")
formatter.write_text(
"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."
)
formatter.write("\n")
formatter.write_text(
'You may specify a null default (e.g. "" or empty string) by omitting the value after '
+ 'the comma, e.g. {title,} which would render to "" if title had no value.'
)
formatter.write("\n") formatter.write("\n")
templ_tuples = [("Substitution", "Description")] templ_tuples = [("Substitution", "Description")]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items()) templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
@@ -284,3 +153,45 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
formatter.write_dl(templ_tuples) formatter.write_dl(templ_tuples)
help_text += formatter.getvalue() help_text += formatter.getvalue()
return help_text return help_text
def template_help(width=78):
"""Return formatted string for template system """
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
template_help_md = strip_md_links(get_template_help())
console.print(Markdown(template_help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def rich_text(text, width=78):
"""Return rich formatted text"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
console.print(text)
rich_text = sio.getvalue()
sio.close()
return rich_text
def strip_md_links(md):
"""strip markdown links from markdown text md
Args:
md: str, markdown text
Returns:
str with markdown links removed
Note: This uses a very basic regex that likely fails on all sorts of edge cases
but works for the links in the osxphotos docs
"""
links = r"(?:[*#])|\[(.*?)\]\(.+?\)"
def subfn(match):
return match.group(1)
return re.sub(links, subfn, md)

View File

@@ -176,6 +176,10 @@ class ExifTool:
command = [f"-{tag}={value}"] command = [f"-{tag}={value}"]
if self.overwrite and not self._context_mgr: if self.overwrite and not self._context_mgr:
command.append("-overwrite_original") command.append("-overwrite_original")
# avoid "Warning: Some character(s) could not be encoded in Latin" warning
command.append("-iptc:codedcharacterset=utf8")
if self._context_mgr: if self._context_mgr:
self._commands.extend(command) self._commands.extend(command)
return True return True
@@ -254,7 +258,11 @@ class ExifTool:
filename = os.fsencode(self.file) if not no_file else b"" filename = os.fsencode(self.file) if not no_file else b""
if self.flags: if self.flags:
command_str = b"\n".join([f.encode("utf-8") for f in self.flags]) # need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
flags = []
for f in self.flags:
flags.extend(f.split())
command_str = b"\n".join([f.encode("utf-8") for f in flags])
command_str += b"\n" command_str += b"\n"
else: else:
command_str = b"" command_str = b""
@@ -311,7 +319,13 @@ class ExifTool:
if not json_str: if not json_str:
return dict() return dict()
exifdict = json.loads(json_str) try:
exifdict = json.loads(json_str)
except Exception as e:
# will fail with some commands, e.g --ext AVI which produces
# 'No file with specified extension' instead of json
return dict()
exifdict = exifdict[0] exifdict = exifdict[0]
if not tag_groups: if not tag_groups:
# strip tag groups # strip tag groups

View File

@@ -11,16 +11,15 @@ MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
class PersonInfo: class PersonInfo:
""" Info about a person in the Photos library """Info about a person in the Photos library"""
"""
def __init__(self, db=None, pk=None): def __init__(self, db=None, pk=None):
""" Creates a new PersonInfo instance """Creates a new PersonInfo instance
Arguments: Arguments:
db: instance of PhotosDB object db: instance of PhotosDB object
pk: primary key value of person to initialize PersonInfo with pk: primary key value of person to initialize PersonInfo with
Returns: Returns:
PersonInfo instance PersonInfo instance
""" """
@@ -57,8 +56,8 @@ class PersonInfo:
@property @property
def face_info(self): def face_info(self):
""" Returns a list of FaceInfo objects associated with this person sorted by quality score """Returns a list of FaceInfo objects associated with this person sorted by quality score
Highest quality face is result[0] and lowest quality face is result[n] Highest quality face is result[0] and lowest quality face is result[n]
""" """
try: try:
faces = self._db._db_faceinfo_person[self._pk] faces = self._db._db_faceinfo_person[self._pk]
@@ -103,16 +102,15 @@ class PersonInfo:
class FaceInfo: class FaceInfo:
""" Info about a face in the Photos library """Info about a face in the Photos library"""
"""
def __init__(self, db=None, pk=None): def __init__(self, db=None, pk=None):
""" Creates a new FaceInfo instance """Creates a new FaceInfo instance
Arguments: Arguments:
db: instance of PhotosDB object db: instance of PhotosDB object
pk: primary key value of face to init the object with pk: primary key value of face to init the object with
Returns: Returns:
FaceInfo instance FaceInfo instance
""" """
@@ -156,7 +154,7 @@ class FaceInfo:
@property @property
def center(self): def center(self):
""" Coordinates, in PIL format, for center of face """Coordinates, in PIL format, for center of face
Returns: Returns:
tuple of coordinates in form (x, y) tuple of coordinates in form (x, y)
@@ -165,7 +163,7 @@ class FaceInfo:
@property @property
def size_pixels(self): def size_pixels(self):
""" Size of face in pixels (centered around center_x, center_y) """Size of face in pixels (centered around center_x, center_y)
Returns: Returns:
size, in int pixels, of a circle drawn around the center of the face size, in int pixels, of a circle drawn around the center of the face
@@ -176,7 +174,7 @@ class FaceInfo:
@property @property
def mouth(self): def mouth(self):
""" Coordinates, in PIL format, for mouth position """Coordinates, in PIL format, for mouth position
Returns: Returns:
tuple of coordinates in form (x, y) tuple of coordinates in form (x, y)
@@ -185,7 +183,7 @@ class FaceInfo:
@property @property
def left_eye(self): def left_eye(self):
""" Coordinates, in PIL format, for left eye position """Coordinates, in PIL format, for left eye position
Returns: Returns:
tuple of coordinates in form (x, y) tuple of coordinates in form (x, y)
@@ -194,7 +192,7 @@ class FaceInfo:
@property @property
def right_eye(self): def right_eye(self):
""" Coordinates, in PIL format, for right eye position """Coordinates, in PIL format, for right eye position
Returns: Returns:
tuple of coordinates in form (x, y) tuple of coordinates in form (x, y)
@@ -223,7 +221,7 @@ class FaceInfo:
@property @property
def mwg_rs_area(self): def mwg_rs_area(self):
""" Get coordinates for Metadata Working Group Region Area. """Get coordinates for Metadata Working Group Region Area.
Returns: Returns:
MWG_RS_Area named tuple with x, y, h, w where: MWG_RS_Area named tuple with x, y, h, w where:
@@ -249,7 +247,7 @@ class FaceInfo:
@property @property
def mpri_reg_rect(self): def mpri_reg_rect(self):
""" Get coordinates for Microsoft Photo Region Rectangle. """Get coordinates for Microsoft Photo Region Rectangle.
Returns: Returns:
MPRI_Reg_Rect named tuple with x, y, h, w where: MPRI_Reg_Rect named tuple with x, y, h, w where:
@@ -278,7 +276,7 @@ class FaceInfo:
return MPRI_Reg_Rect(x, y, h, w) return MPRI_Reg_Rect(x, y, h, w)
def face_rect(self): def face_rect(self):
""" Get face rectangle coordinates for current version of the associated image """Get face rectangle coordinates for current version of the associated image
If image has been edited, rectangle applies to edited version, otherwise original version If image has been edited, rectangle applies to edited version, otherwise original version
Coordinates in format and reference frame used by PIL Coordinates in format and reference frame used by PIL
@@ -321,12 +319,12 @@ class FaceInfo:
return yaw return yaw
def _fix_orientation(self, xy): def _fix_orientation(self, xy):
""" Translate an (x, y) tuple based on image orientation """Translate an (x, y) tuple based on image orientation
Arguments: Arguments:
xy: tuple of (x, y) coordinates for point to translate xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width) in format used by Photos (percent of height/width)
Returns: Returns:
(x, y) tuple of translated coordinates (x, y) tuple of translated coordinates
""" """
@@ -350,21 +348,24 @@ class FaceInfo:
elif orientation == 7: elif orientation == 7:
x, y = y, x x, y = y, x
y = 1.0 - y y = 1.0 - y
elif orientation ==8: elif orientation == 8:
x, y = y, x x, y = y, x
elif orientation == 0:
# set by osxphotos if adjusted orientation cannot be read, assume it's 1
y = 1.0 - y
else: else:
logging.warning(f"Unhandled orientation: {orientation}") logging.warning(f"Unhandled orientation: {orientation}")
return (x, y) return (x, y)
def _make_point(self, xy): def _make_point(self, xy):
""" Translate an (x, y) tuple based on image orientation """Translate an (x, y) tuple based on image orientation
and convert to image coordinates and convert to image coordinates
Arguments: Arguments:
xy: tuple of (x, y) coordinates for point to translate xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width) in format used by Photos (percent of height/width)
Returns: Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame (x, y) tuple of translated coordinates in pixels in PIL format/reference frame
""" """
@@ -379,13 +380,13 @@ class FaceInfo:
return (int(x * dx), int(y * dy)) return (int(x * dx), int(y * dy))
def _make_point_with_rotation(self, xy): def _make_point_with_rotation(self, xy):
""" Translate an (x, y) tuple based on image orientation and rotation """Translate an (x, y) tuple based on image orientation and rotation
and convert to image coordinates and convert to image coordinates
Arguments: Arguments:
xy: tuple of (x, y) coordinates for point to translate xy: tuple of (x, y) coordinates for point to translate
in format used by Photos (percent of height/width) in format used by Photos (percent of height/width)
Returns: Returns:
(x, y) tuple of translated coordinates in pixels in PIL format/reference frame (x, y) tuple of translated coordinates in pixels in PIL format/reference frame
""" """
@@ -472,14 +473,14 @@ class FaceInfo:
def rotate_image_point(x, y, xmid, ymid, angle): def rotate_image_point(x, y, xmid, ymid, angle):
""" rotate image point about xm, ym by angle in radians """rotate image point about xm, ym by angle in radians
Arguments: Arguments:
x: x coordinate of point to rotate x: x coordinate of point to rotate
y: y coordinate of point to rotate y: y coordinate of point to rotate
xmid: x coordinate of center point to rotate about xmid: x coordinate of center point to rotate about
ymid: y coordinate of center point to rotate about ymid: y coordinate of center point to rotate about
angle: angle in radians about which to coordinate, angle: angle in radians about which to coordinate,
counter-clockwise is positive counter-clockwise is positive
Returns: Returns:

View File

@@ -87,6 +87,8 @@ class ExportResults:
exiftool_error=None, exiftool_error=None,
xattr_written=None, xattr_written=None,
xattr_skipped=None, xattr_skipped=None,
deleted_files=None,
deleted_directories=None,
): ):
self.exported = exported or [] self.exported = exported or []
self.new = new or [] self.new = new or []
@@ -107,6 +109,8 @@ class ExportResults:
self.exiftool_error = exiftool_error or [] self.exiftool_error = exiftool_error or []
self.xattr_written = xattr_written or [] self.xattr_written = xattr_written or []
self.xattr_skipped = xattr_skipped or [] self.xattr_skipped = xattr_skipped or []
self.deleted_files = deleted_files or []
self.deleted_directories = deleted_directories or []
def all_files(self): def all_files(self):
""" return all filenames contained in results """ """ return all filenames contained in results """
@@ -151,6 +155,8 @@ class ExportResults:
self.error += other.error self.error += other.error
self.exiftool_warning += other.exiftool_warning self.exiftool_warning += other.exiftool_warning
self.exiftool_error += other.exiftool_error self.exiftool_error += other.exiftool_error
self.deleted_files += other.deleted_files
self.deleted_directories += other.deleted_directories
return self return self
def __str__(self): def __str__(self):
@@ -173,6 +179,8 @@ class ExportResults:
+ f",error={self.error}" + f",error={self.error}"
+ f",exiftool_warning={self.exiftool_warning}" + f",exiftool_warning={self.exiftool_warning}"
+ f",exiftool_error={self.exiftool_error}" + f",exiftool_error={self.exiftool_error}"
+ f",deleted_files={self.deleted_files}"
+ f",deleted_directories={self.deleted_directories}"
+ ")" + ")"
) )
@@ -475,6 +483,9 @@ def export2(
merge_exif_keywords=False, merge_exif_keywords=False,
merge_exif_persons=False, merge_exif_persons=False,
jpeg_ext=None, jpeg_ext=None,
persons=True,
location=True,
replace_keywords=False,
): ):
"""export photo, like export but with update and dry_run options """export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised dest: must be valid destination path or exception raised
@@ -527,6 +538,9 @@ def export2(
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading "." jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading "."
persons: if True, include persons in exported metadata
location: if True, include location in exported metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
Returns: ExportResults class Returns: ExportResults class
ExportResults has attributes: ExportResults has attributes:
@@ -562,11 +576,11 @@ def export2(
if export_db is None: if export_db is None:
export_db = ExportDBNoOp() export_db = ExportDBNoOp()
if verbose is None: if verbose and not callable(verbose):
verbose = noop
elif not callable(verbose):
raise TypeError("verbose must be callable") raise TypeError("verbose must be callable")
self._verbose = verbose
if verbose is None:
verbose = self._verbose
# suffix to add to edited files # suffix to add to edited files
# e.g. name will be filename_edited.jpg # e.g. name will be filename_edited.jpg
@@ -607,9 +621,9 @@ def export2(
) )
edited_name = pathlib.Path(self.path_edited).name edited_name = pathlib.Path(self.path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
fname = pathlib.Path(self.filename).stem + edited_identifier + edited_suffix fname = pathlib.Path(self.original_filename).stem + edited_identifier + edited_suffix
else: else:
fname = self.filename fname = self.original_filename
uti = self.uti if edited else self.uti_original uti = self.uti if edited else self.uti_original
if convert_to_jpeg and self.isphoto and uti != "public.jpeg": if convert_to_jpeg and self.isphoto and uti != "public.jpeg":
@@ -941,6 +955,9 @@ def export2(
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
filename=dest.name, filename=dest.name,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
sidecars.append( sidecars.append(
( (
@@ -964,6 +981,9 @@ def export2(
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
filename=dest.name, filename=dest.name,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
sidecars.append( sidecars.append(
( (
@@ -983,6 +1003,9 @@ def export2(
keyword_template=keyword_template, keyword_template=keyword_template,
description_template=description_template, description_template=description_template,
extension=dest.suffix[1:] if dest.suffix else None, extension=dest.suffix[1:] if dest.suffix else None,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
sidecars.append( sidecars.append(
( (
@@ -1050,6 +1073,9 @@ def export2(
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
)[0] )[0]
if old_data != current_data: if old_data != current_data:
@@ -1070,6 +1096,9 @@ def export2(
flags=exiftool_flags, flags=exiftool_flags,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
if warning_: if warning_:
all_results.exiftool_warning.append((exported_file, warning_)) all_results.exiftool_warning.append((exported_file, warning_))
@@ -1087,6 +1116,9 @@ def export2(
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
), ),
) )
export_db.set_stat_exif_for_file( export_db.set_stat_exif_for_file(
@@ -1109,6 +1141,9 @@ def export2(
flags=exiftool_flags, flags=exiftool_flags,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
if warning_: if warning_:
all_results.exiftool_warning.append((exported_file, warning_)) all_results.exiftool_warning.append((exported_file, warning_))
@@ -1126,6 +1161,9 @@ def export2(
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
), ),
) )
export_db.set_stat_exif_for_file( export_db.set_stat_exif_for_file(
@@ -1345,6 +1383,9 @@ def _write_exif_data(
flags=None, flags=None,
merge_exif_keywords=False, merge_exif_keywords=False,
merge_exif_persons=False, merge_exif_persons=False,
persons=True,
location=True,
replace_keywords=False,
): ):
"""write exif data to image file at filepath """write exif data to image file at filepath
@@ -1355,6 +1396,9 @@ def _write_exif_data(
keyword_template: (list of strings); list of template strings to render as keywords keyword_template: (list of strings); list of template strings to render as keywords
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F) flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)
persons: if True, write person data to metadata
location: if True, write location data to metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
Returns: Returns:
(warning, error) of warning and error strings if exiftool produces warnings or errors (warning, error) of warning and error strings if exiftool produces warnings or errors
@@ -1369,6 +1413,9 @@ def _write_exif_data(
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool: with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool:
@@ -1391,6 +1438,9 @@ def _exiftool_dict(
merge_exif_keywords=False, merge_exif_keywords=False,
merge_exif_persons=False, merge_exif_persons=False,
filename=None, filename=None,
persons=True,
location=True,
replace_keywords=False,
): ):
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool. """Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
Does not include all the EXIF fields as those are likely already in the image. Does not include all the EXIF fields as those are likely already in the image.
@@ -1404,6 +1454,9 @@ def _exiftool_dict(
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
merge_exif_keywords: merge keywords in the file's exif metadata (requires exiftool) merge_exif_keywords: merge keywords in the file's exif metadata (requires exiftool)
merge_exif_persons: merge persons in the file's exif metadata (requires exiftool) merge_exif_persons: merge persons in the file's exif metadata (requires exiftool)
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
Returns: dict with exiftool tags / values Returns: dict with exiftool tags / values
@@ -1411,8 +1464,10 @@ def _exiftool_dict(
EXIF:ImageDescription (may include template) EXIF:ImageDescription (may include template)
XMP:Description (may include template) XMP:Description (may include template)
XMP:Title XMP:Title
IPTC:ObjectName
XMP:TagsList (may include album name, person name, or template) XMP:TagsList (may include album name, person name, or template)
IPTC:Keywords (may include album name, person name, or template) IPTC:Keywords (may include album name, person name, or template)
IPTC:Caption-Abstract
XMP:Subject (set to keywords + persons) XMP:Subject (set to keywords + persons)
XMP:PersonInImage XMP:PersonInImage
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
@@ -1428,6 +1483,9 @@ def _exiftool_dict(
QuickTime:ModifyDate (UTC) QuickTime:ModifyDate (UTC)
QuickTime:GPSCoordinates QuickTime:GPSCoordinates
UserData:GPSCoordinates UserData:GPSCoordinates
Reference:
https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf
""" """
exif = ( exif = (
@@ -1447,30 +1505,34 @@ def _exiftool_dict(
description = " ".join(rendered) if rendered else "" description = " ".join(rendered) if rendered else ""
exif["EXIF:ImageDescription"] = description exif["EXIF:ImageDescription"] = description
exif["XMP:Description"] = description exif["XMP:Description"] = description
exif["IPTC:Caption-Abstract"] = description
elif self.description: elif self.description:
exif["EXIF:ImageDescription"] = self.description exif["EXIF:ImageDescription"] = self.description
exif["XMP:Description"] = self.description exif["XMP:Description"] = self.description
exif["IPTC:Caption-Abstract"] = self.description
if self.title: if self.title:
exif["XMP:Title"] = self.title exif["XMP:Title"] = self.title
exif["IPTC:ObjectName"] = self.title
keyword_list = [] keyword_list = []
if merge_exif_keywords: if merge_exif_keywords:
keyword_list.extend(self._get_exif_keywords()) keyword_list.extend(self._get_exif_keywords())
if self.keywords: if self.keywords and not replace_keywords:
keyword_list.extend(self.keywords) keyword_list.extend(self.keywords)
person_list = [] person_list = []
if merge_exif_persons: if persons:
person_list.extend(self._get_exif_persons()) if merge_exif_persons:
person_list.extend(self._get_exif_persons())
if self.persons: if self.persons:
# filter out _UNKNOWN_PERSON # filter out _UNKNOWN_PERSON
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON]) person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
if use_persons_as_keywords and person_list: if use_persons_as_keywords and person_list:
keyword_list.extend(person_list) keyword_list.extend(person_list)
if use_albums_as_keywords and self.albums: if use_albums_as_keywords and self.albums:
keyword_list.extend(self.albums) keyword_list.extend(self.albums)
@@ -1501,8 +1563,8 @@ def _exiftool_dict(
if len(long_str) > _MAX_IPTC_KEYWORD_LEN if len(long_str) > _MAX_IPTC_KEYWORD_LEN
] ]
if long_keywords: if long_keywords:
logging.warning( self._verbose(
f"Some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN}: {long_keywords}" f"Warning: some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN} (exiftool will truncate these): {long_keywords}"
) )
keyword_list.extend(rendered_keywords) keyword_list.extend(rendered_keywords)
@@ -1514,25 +1576,26 @@ def _exiftool_dict(
exif["XMP:Subject"] = keyword_list.copy() exif["XMP:Subject"] = keyword_list.copy()
exif["XMP:TagsList"] = keyword_list.copy() exif["XMP:TagsList"] = keyword_list.copy()
if person_list: if persons and person_list:
person_list = sorted(list(set(person_list))) person_list = sorted(list(set(person_list)))
exif["XMP:PersonInImage"] = person_list.copy() exif["XMP:PersonInImage"] = person_list.copy()
# if self.favorite(): # if self.favorite():
# exif["Rating"] = 5 # exif["Rating"] = 5
(lat, lon) = self.location if location:
if lat is not None and lon is not None: (lat, lon) = self.location
if self.isphoto: if lat is not None and lon is not None:
exif["EXIF:GPSLatitude"] = lat if self.isphoto:
exif["EXIF:GPSLongitude"] = lon exif["EXIF:GPSLatitude"] = lat
lat_ref = "N" if lat >= 0 else "S" exif["EXIF:GPSLongitude"] = lon
lon_ref = "E" if lon >= 0 else "W" lat_ref = "N" if lat >= 0 else "S"
exif["EXIF:GPSLatitudeRef"] = lat_ref lon_ref = "E" if lon >= 0 else "W"
exif["EXIF:GPSLongitudeRef"] = lon_ref exif["EXIF:GPSLatitudeRef"] = lat_ref
elif self.ismovie: exif["EXIF:GPSLongitudeRef"] = lon_ref
exif["Keys:GPSCoordinates"] = f"{lat} {lon}" elif self.ismovie:
exif["UserData:GPSCoordinates"] = f"{lat} {lon}" exif["Keys:GPSCoordinates"] = f"{lat} {lon}"
exif["UserData:GPSCoordinates"] = f"{lat} {lon}"
# process date/time and timezone offset # process date/time and timezone offset
# Photos exports the following fields and sets modify date to creation date # Photos exports the following fields and sets modify date to creation date
@@ -1591,6 +1654,13 @@ def _exiftool_dict(
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc( exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
self.date_modified self.date_modified
).strftime("%Y:%m:%d %H:%M:%S") ).strftime("%Y:%m:%d %H:%M:%S")
# remove any new lines in any fields
for field, val in exif.items():
if type(val) == str:
exif[field] = val.replace("\n", " ")
elif type(val) == list:
exif[field] = [v.replace("\n", " ") for v in val]
return exif return exif
@@ -1640,6 +1710,9 @@ def _exiftool_json_sidecar(
merge_exif_keywords=False, merge_exif_keywords=False,
merge_exif_persons=False, merge_exif_persons=False,
filename=None, filename=None,
persons=True,
location=True,
replace_keywords=False,
): ):
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool. """Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
Does not include all the EXIF fields as those are likely already in the image. Does not include all the EXIF fields as those are likely already in the image.
@@ -1654,13 +1727,18 @@ def _exiftool_json_sidecar(
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
filename: filename of the destination image file for including in exiftool signature in JSON sidecar filename: filename of the destination image file for including in exiftool signature in JSON sidecar
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
Returns: dict with exiftool tags / values Returns: dict with exiftool tags / values
Exports the following: Exports the following:
EXIF:ImageDescription EXIF:ImageDescription
XMP:Description (may include template) XMP:Description (may include template)
IPTC:CaptionAbstract
XMP:Title XMP:Title
IPTC:ObjectName
XMP:TagsList XMP:TagsList
IPTC:Keywords (may include album name, person name, or template) IPTC:Keywords (may include album name, person name, or template)
XMP:Subject (set to keywords + person) XMP:Subject (set to keywords + person)
@@ -1688,6 +1766,9 @@ def _exiftool_json_sidecar(
merge_exif_keywords=merge_exif_keywords, merge_exif_keywords=merge_exif_keywords,
merge_exif_persons=merge_exif_persons, merge_exif_persons=merge_exif_persons,
filename=filename, filename=filename,
persons=persons,
location=location,
replace_keywords=replace_keywords,
) )
if not tag_groups: if not tag_groups:
@@ -1710,6 +1791,9 @@ def _xmp_sidecar(
extension=None, extension=None,
merge_exif_keywords=False, merge_exif_keywords=False,
merge_exif_persons=False, merge_exif_persons=False,
persons=True,
location=True,
replace_keywords=False,
): ):
"""returns string for XMP sidecar """returns string for XMP sidecar
use_albums_as_keywords: treat album names as keywords use_albums_as_keywords: treat album names as keywords
@@ -1719,6 +1803,9 @@ def _xmp_sidecar(
extension: which extension to use for SidecarForExtension property extension: which extension to use for SidecarForExtension property
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
""" """
xmp_template_file = ( xmp_template_file = (
@@ -1742,22 +1829,23 @@ def _xmp_sidecar(
if merge_exif_keywords: if merge_exif_keywords:
keyword_list.extend(self._get_exif_keywords()) keyword_list.extend(self._get_exif_keywords())
if self.keywords: if self.keywords and not replace_keywords:
keyword_list.extend(self.keywords) keyword_list.extend(self.keywords)
# TODO: keyword handling in this and _exiftool_json_sidecar is # TODO: keyword handling in this and _exiftool_json_sidecar is
# good candidate for pulling out in a function # good candidate for pulling out in a function
person_list = [] person_list = []
if merge_exif_persons: if persons:
person_list.extend(self._get_exif_persons()) if merge_exif_persons:
person_list.extend(self._get_exif_persons())
if self.persons: if self.persons:
# filter out _UNKNOWN_PERSON # filter out _UNKNOWN_PERSON
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON]) person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
if use_persons_as_keywords and person_list: if use_persons_as_keywords and person_list:
keyword_list.extend(person_list) keyword_list.extend(person_list)
if use_albums_as_keywords and self.albums: if use_albums_as_keywords and self.albums:
keyword_list.extend(self.albums) keyword_list.extend(self.albums)
@@ -1781,28 +1869,20 @@ def _xmp_sidecar(
if _OSXPHOTOS_NONE_SENTINEL not in keyword if _OSXPHOTOS_NONE_SENTINEL not in keyword
] ]
# check to see if any keywords too long
long_keywords = [
long_str
for long_str in rendered_keywords
if len(long_str) > _MAX_IPTC_KEYWORD_LEN
]
if long_keywords:
logging.warning(
f"Some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN}: {long_keywords}"
)
keyword_list.extend(rendered_keywords) keyword_list.extend(rendered_keywords)
# remove duplicates # remove duplicates
# sorted mainly to make testing the XMP file easier # sorted mainly to make testing the XMP file easier
if keyword_list: if keyword_list:
keyword_list = sorted(list(set(keyword_list))) keyword_list = sorted(list(set(keyword_list)))
if person_list: if persons and person_list:
person_list = sorted(list(set(person_list))) person_list = sorted(list(set(person_list)))
subject_list = keyword_list subject_list = keyword_list
if location:
latlon = self.location
xmp_str = xmp_template.render( xmp_str = xmp_template.render(
photo=self, photo=self,
description=description, description=description,
@@ -1810,6 +1890,7 @@ def _xmp_sidecar(
persons=person_list, persons=person_list,
subjects=subject_list, subjects=subject_list,
extension=extension, extension=extension,
location=latlon,
version=__version__, version=__version__,
) )

View File

@@ -27,6 +27,7 @@ from .._constants import (
_PHOTOS_5_SHARED_PHOTO_PATH, _PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION, _PHOTOS_5_VERSION,
) )
from ..adjustmentsinfo import AdjustmentsInfo
from ..albuminfo import AlbumInfo, ImportInfo from ..albuminfo import AlbumInfo, ImportInfo
from ..personinfo import FaceInfo, PersonInfo from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate from ..phototemplate import PhotoTemplate
@@ -70,6 +71,7 @@ class PhotoInfo:
self._uuid = uuid self._uuid = uuid
self._info = info self._info = info
self._db = db self._db = db
self._verbose = self._db._verbose
@property @property
def filename(self): def filename(self):
@@ -451,9 +453,24 @@ class PhotoInfo:
) )
return self._albums return self._albums
@property
def burst_albums(self):
"""If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums"""
if self.burst_selected or not self.burst:
return self.albums
try:
return self._burst_albums
except AttributeError:
burst_albums = []
for photo in self.burst_photos:
burst_albums.extend(photo.albums)
self._burst_albums = list(set(burst_albums))
return self._burst_albums
@property @property
def album_info(self): def album_info(self):
""" list of AlbumInfo objects representing albums the photos is contained in """ """ list of AlbumInfo objects representing albums the photo is contained in """
try: try:
return self._album_info return self._album_info
except AttributeError: except AttributeError:
@@ -463,6 +480,21 @@ class PhotoInfo:
] ]
return self._album_info return self._album_info
@property
def burst_album_info(self):
""" If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info """
if self.burst_selected or not self.burst:
return self.album_info
try:
return self._burst_album_info
except AttributeError:
burst_album_info = []
for photo in self.burst_photos:
burst_album_info.extend(photo.album_info)
self._burst_album_info = list(set(burst_album_info))
return self._burst_album_info
@property @property
def import_info(self): def import_info(self):
""" ImportInfo object representing import session for the photo or None if no import session """ """ ImportInfo object representing import session for the photo or None if no import session """
@@ -509,6 +541,30 @@ class PhotoInfo:
""" True if picture has adjustments / edits """ """ True if picture has adjustments / edits """
return self._info["hasAdjustments"] == 1 return self._info["hasAdjustments"] == 1
@property
def adjustments(self):
""" Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only """
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
if self.hasadjustments:
try:
return self._adjustmentinfo
except AttributeError:
library = self._db._library_path
directory = self._uuid[0] # first char of uuid
plist_file = (
pathlib.Path(library)
/ "resources"
/ "renders"
/ directory
/ f"{self._uuid}.plist"
)
if not plist_file.is_file():
return None
self._adjustmentinfo = AdjustmentsInfo(plist_file)
return self._adjustmentinfo
@property @property
def external_edit(self): def external_edit(self):
""" Returns True if picture was edited outside of Photos using external editor """ """ Returns True if picture was edited outside of Photos using external editor """
@@ -654,6 +710,11 @@ class PhotoInfo:
""" Returns True if photo is part of a Burst photo set, otherwise False """ """ Returns True if photo is part of a Burst photo set, otherwise False """
return self._info["burst"] return self._info["burst"]
@property
def burst_selected(self):
""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """
return self._info["burst_key"]
@property @property
def burst_photos(self): def burst_photos(self):
"""If photo is a burst photo, returns list of PhotoInfo objects """If photo is a burst photo, returns list of PhotoInfo objects
@@ -822,8 +883,19 @@ class PhotoInfo:
@property @property
def orientation(self): def orientation(self):
""" returns EXIF orientation of the current photo version as int """ """ returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined """
return self._info["orientation"] if self._db._db_version <= _PHOTOS_4_VERSION:
return self._info["orientation"]
# For Photos 5+, try to get the adjusted orientation
if self.hasadjustments:
if self.adjustments:
return self.adjustments.adj_orientation
else:
# can't reliably determine orientation for edited photo if adjustmentinfo not available
return 0
else:
return self._info["orientation"]
@property @property
def original_height(self): def original_height(self):

View File

@@ -1828,7 +1828,6 @@ class PhotosDB:
# get details about photos # get details about photos
verbose("Processing photo details.") verbose("Processing photo details.")
logging.debug(f"Getting information about photos")
c.execute( c.execute(
f"""SELECT {asset_table}.ZUUID, f"""SELECT {asset_table}.ZUUID,
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT, ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
@@ -2041,6 +2040,7 @@ class PhotosDB:
# > 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE) # > 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)
info["customRenderedValue"] = row[22] info["customRenderedValue"] = row[22]
info["hdr"] = True if row[22] == 3 else False info["hdr"] = True if row[22] == 3 else False
info["depth_state"] = row[36]
info["portrait"] = True if row[36] != 0 else False info["portrait"] = True if row[36] != 0 else False
# Set panorama from either KindSubType or RenderedValue # Set panorama from either KindSubType or RenderedValue
@@ -2735,8 +2735,6 @@ class PhotosDB:
# an empty album will be in _dbalbum_titles but not _dbalbums_album # an empty album will be in _dbalbum_titles but not _dbalbums_album
pass pass
album_set.update(title_set) album_set.update(title_set)
else:
logging.debug(f"Could not find album '{album}' in database")
photos_sets.append(album_set) photos_sets.append(album_set)
if uuid: if uuid:
@@ -2744,8 +2742,6 @@ class PhotosDB:
for u in uuid: for u in uuid:
if u in self._dbphotos: if u in self._dbphotos:
uuid_set.update([u]) uuid_set.update([u])
else:
logging.debug(f"Could not find uuid '{u}' in database")
photos_sets.append(uuid_set) photos_sets.append(uuid_set)
if keywords: if keywords:
@@ -2753,8 +2749,6 @@ class PhotosDB:
for keyword in keywords: for keyword in keywords:
if keyword in self._dbkeywords_keyword: if keyword in self._dbkeywords_keyword:
keyword_set.update(self._dbkeywords_keyword[keyword]) keyword_set.update(self._dbkeywords_keyword[keyword])
else:
logging.debug(f"Could not find keyword '{keyword}' in database")
photos_sets.append(keyword_set) photos_sets.append(keyword_set)
if persons: if persons:
@@ -2767,8 +2761,6 @@ class PhotosDB:
except KeyError: except KeyError:
# some persons have zero photos so they won't be in _dbfaces_pk # some persons have zero photos so they won't be in _dbfaces_pk
pass pass
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set) photos_sets.append(person_set)
if from_date or to_date: # sourcery off if from_date or to_date: # sourcery off
@@ -2779,14 +2771,10 @@ class PhotosDB:
dsel = { dsel = {
k: v for k, v in dsel.items() if v["imageDate"] >= from_date k: v for k, v in dsel.items() if v["imageDate"] >= from_date
} }
logging.debug(
f"Found %i items with from_date {from_date}" % len(dsel)
)
if to_date: if to_date:
if not datetime_has_tz(to_date): if not datetime_has_tz(to_date):
to_date = datetime_naive_to_local(to_date) to_date = datetime_naive_to_local(to_date)
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date} dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date}
logging.debug(f"Found %i items with to_date {to_date}" % len(dsel))
photos_sets.append(set(dsel.keys())) photos_sets.append(set(dsel.keys()))
photoinfo = [] photoinfo = []

View File

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

File diff suppressed because it is too large Load Diff

122
osxphotos/phototemplate.tx Normal file
View File

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

View File

@@ -99,12 +99,6 @@
% endif % endif
</%def> </%def>
<%def name="orientation(orientation)">
% if orientation is not None:
<tiff:Orientation>${orientation}</tiff:Orientation>
% endif
</%def>
<%def name="mwg_face_regions(photo)"> <%def name="mwg_face_regions(photo)">
% if photo.face_info: % if photo.face_info:
<mwg-rs:Regions rdf:parseType="Resource"> <mwg-rs:Regions rdf:parseType="Resource">
@@ -182,12 +176,7 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
${gps_info(*photo.location)} ${gps_info(*location)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
${orientation(photo.orientation)}
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""

View File

@@ -99,12 +99,6 @@
% endif % endif
</%def> </%def>
<%def name="orientation(orientation)">
% if orientation is not None:
<tiff:Orientation>${orientation}</tiff:Orientation>
% endif
</%def>
<%def name="mwg_face_regions(photo)"> <%def name="mwg_face_regions(photo)">
% if photo.face_info: % if photo.face_info:
<mwg-rs:Regions rdf:parseType="Resource"> <mwg-rs:Regions rdf:parseType="Resource">
@@ -182,12 +176,7 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
${gps_info(*photo.location)} ${gps_info(*location)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
${orientation(photo.orientation)}
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""

View File

@@ -9,7 +9,6 @@ atomicwrites==1.3.0
attrs==19.1.0 attrs==19.1.0
backcall==0.1.0 backcall==0.1.0
better-exceptions-fork==0.2.1.post6 better-exceptions-fork==0.2.1.post6
black==19.10b0
bleach==3.3.0 bleach==3.3.0
bpylist2==3.0.2 bpylist2==3.0.2
certifi==2020.4.5.1 certifi==2020.4.5.1
@@ -50,7 +49,7 @@ pathvalidate==2.2.1
pexpect==4.8.0 pexpect==4.8.0
photoscript==0.1.0 photoscript==0.1.0
pickleshare==0.7.5 pickleshare==0.7.5
Pillow==7.2.0 Pillow==8.1.1
pkginfo==1.5.0.1 pkginfo==1.5.0.1
pluggy==0.12.0 pluggy==0.12.0
prompt-toolkit==3.0.4 prompt-toolkit==3.0.4
@@ -60,7 +59,7 @@ py==1.8.0
py2app==0.21 py2app==0.21
pycparser==2.20 pycparser==2.20
pyfiglet==0.8.post1 pyfiglet==0.8.post1
Pygments==2.6.1 Pygments==2.7.4
PyInstaller==3.6 PyInstaller==3.6
pyinstaller-setuptools==2019.3 pyinstaller-setuptools==2019.3
pylint==2.3.1 pylint==2.3.1
@@ -182,14 +181,16 @@ pyobjc-framework-Vision==6.2.2
pyobjc-framework-WebKit==6.2.2 pyobjc-framework-WebKit==6.2.2
pyparsing==2.4.1.1 pyparsing==2.4.1.1
python-dateutil==2.8.1 python-dateutil==2.8.1
PyYAML==5.1.2 PyYAML==5.4
pyzmq==18.1.1 pyzmq==18.1.1
readme-renderer==25.0 readme-renderer==25.0
regex==2020.2.20 regex==2020.2.20
requests==2.23.0 requests==2.23.0
requests-toolbelt==0.9.1 requests-toolbelt==0.9.1
rich==9.11.1
six==1.14.0 six==1.14.0
termcolor==1.1.0 termcolor==1.1.0
textx==2.3.0
toml==0.10.0 toml==0.10.0
tornado==6.0.4 tornado==6.0.4
tox==3.19.0 tox==3.19.0

View File

@@ -84,6 +84,8 @@ setup(
"photoscript>=0.1.0", "photoscript>=0.1.0",
"toml>=0.10.0", "toml>=0.10.0",
"osxmetadata>=0.99.13", "osxmetadata>=0.99.13",
"textx==2.3.0",
"rich>=9.11.1",
], ],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]}, entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
include_package_data=True, include_package_data=True,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 528 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

48
tests/photoinfo_mock.py Normal file
View File

@@ -0,0 +1,48 @@
"""Selectively mock a PhotoInfo object"""
from osxphotos import PhotoInfo
class PhotoInfoMock(PhotoInfo):
def __init__(self, photo, **kwargs):
self._photo = photo
self._db = photo._db
self._info = photo._info
for kw in kwargs:
if hasattr(photo, kw):
setattr(self, f"_mock_{kw}", kwargs[kw])
else:
raise ValueError(f"Not a PhotoInfo attribute: {kw}")
@property
def hdr(self):
return (
self._mock_hdr
if getattr(self, "_mock_hdr", None) is not None
else self._photo.hdr
)
@property
def hasadjustments(self):
return (
self._mock_hasadjustments
if getattr(self, "_mock_hasadjustments", None) is not None
else self._photo.hasadjustments
)
@property
def keywords(self):
return (
self._mock_keywords
if getattr(self, "_mock_keywords", None) is not None
else self._photo.keywords
)
@property
def title(self):
return (
self._mock_title
if getattr(self, "_mock_title", None) is not None
else self._photo.title
)

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}] [{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -46,10 +46,6 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:Orientation>1</tiff:Orientation>
</rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}] [{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -52,10 +52,6 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:Orientation>1</tiff:Orientation>
</rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"

View File

@@ -46,10 +46,6 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:Orientation>1</tiff:Orientation>
</rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}] [{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}] [{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -54,10 +54,6 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:Orientation>1</tiff:Orientation>
</rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"

View File

@@ -1 +1 @@
[{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Title": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}] [{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Caption-Abstract": "Girl holding pumpkin", "Title": "I found one!", "ObjectName": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}] [{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -48,10 +48,6 @@
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:exif='http://ns.adobe.com/exif/1.0/'> xmlns:exif='http://ns.adobe.com/exif/1.0/'>
</rdf:Description> </rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:Orientation>1</tiff:Orientation>
</rdf:Description>
<rdf:Description rdf:about="" <rdf:Description rdf:about=""
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"

View File

@@ -1 +1 @@
[{"XMP:Title": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}] [{"XMP:Title": "St. James's Park", "IPTC:ObjectName": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]

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