Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7c5320587 | ||
|
|
cd710771cd | ||
|
|
663e33bc17 | ||
|
|
3660b6360a | ||
|
|
11459d1da4 | ||
|
|
fd14242022 | ||
|
|
6ac311199e | ||
|
|
13df6a2395 | ||
|
|
28dce72a67 | ||
|
|
e5548ed160 | ||
|
|
5714509765 | ||
|
|
46b62af4e2 | ||
|
|
01ea88fe57 | ||
|
|
e6d043ab65 | ||
|
|
5b1174db5d | ||
|
|
9cff8e89c6 | ||
|
|
1553563629 | ||
|
|
db262f58b0 |
106
.all-contributorsrc
Normal file
106
.all-contributorsrc
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"projectName": "osxphotos",
|
||||
"projectOwner": "RhetTbull",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
"commitConvention": "none",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "britiscurious",
|
||||
"name": "britiscurious",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/25646439?v=4",
|
||||
"profile": "https://github.com/britiscurious",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mwort",
|
||||
"name": "Michel Wortmann",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/8170417?v=4",
|
||||
"profile": "https://github.com/mwort",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PabloKohan",
|
||||
"name": "Pablo 'merKur' Kohan",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/8790976?v=4",
|
||||
"profile": "https://github.com/PabloKohan",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hshore29",
|
||||
"name": "hshore29",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/7023497?v=4",
|
||||
"profile": "https://github.com/hshore29",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dmd",
|
||||
"name": "Daniel M. Drucker",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/41439?v=4",
|
||||
"profile": "http://3e.org/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jystervinou",
|
||||
"name": "Jean-Yves Stervinou",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/132356?v=4",
|
||||
"profile": "https://github.com/jystervinou",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dethi",
|
||||
"name": "Thibault Deutsch",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1011520?v=4",
|
||||
"profile": "https://dethi.me/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "grundsch",
|
||||
"name": "grundsch",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/3874928?v=4",
|
||||
"profile": "https://github.com/grundsch",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "agprimatic",
|
||||
"name": "Ag Primatic",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/4685054?v=4",
|
||||
"profile": "https://github.com/agprimatic",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hhoeck",
|
||||
"name": "Horst Höck",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6313998?v=4",
|
||||
"profile": "https://github.com/hhoeck",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7
|
||||
}
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.36.5](https://github.com/RhetTbull/osxphotos/compare/v0.36.4...v0.36.5)
|
||||
|
||||
> 1 November 2020
|
||||
|
||||
- Added --ignore-date-modified flag, issue #247 [`663e33b`](https://github.com/RhetTbull/osxphotos/commit/663e33bc1709f767e1a08242f6bfe86a3fc78552)
|
||||
|
||||
#### [v0.36.4](https://github.com/RhetTbull/osxphotos/compare/v0.36.2...v0.36.4)
|
||||
|
||||
> 1 November 2020
|
||||
|
||||
- Updated --exiftool to set dates/times as Photos does, issue #247 [`11459d1`](https://github.com/RhetTbull/osxphotos/commit/11459d1da4d7d13e36e9db4bdc940b74baad9d11)
|
||||
- Partial fix for issue #247 on Mojave [`6ac3111`](https://github.com/RhetTbull/osxphotos/commit/6ac311199e9f7afe6170cbbd68ceaa1bb9f0682b)
|
||||
- Add @mwort as a contributor [`9cff8e8`](https://github.com/RhetTbull/osxphotos/commit/9cff8e89c6e939d3d371a4f60649f6e5595a55b9)
|
||||
|
||||
#### [v0.36.2](https://github.com/RhetTbull/osxphotos/compare/v0.36.1...v0.36.2)
|
||||
|
||||
> 31 October 2020
|
||||
|
||||
- Fixed handling of date_modified for Catalina, issue #247 [`0cce234`](https://github.com/RhetTbull/osxphotos/commit/0cce234a8cbba63dc1cba439c06fe9de078ff480)
|
||||
|
||||
#### [v0.36.1](https://github.com/RhetTbull/osxphotos/compare/v0.36.0...v0.36.1)
|
||||
|
||||
> 30 October 2020
|
||||
|
||||
- Added --has-comment/--has-likes to CLI, issue #240 [`c5dba8c`](https://github.com/RhetTbull/osxphotos/commit/c5dba8c89bba35d7a77e087b180b2a3d7b94280a)
|
||||
- Cleaned up as_dict/asdict, issue #144, #188 [`603dabb`](https://github.com/RhetTbull/osxphotos/commit/603dabb8f420a89e993d5aadcd3a5614bbb262dd)
|
||||
- Updated README.md [`d16932d`](https://github.com/RhetTbull/osxphotos/commit/d16932d0fd8d160ccf44e9842329d5933dc25b36)
|
||||
|
||||
#### [v0.36.0](https://github.com/RhetTbull/osxphotos/compare/v0.35.7...v0.36.0)
|
||||
|
||||
> 26 October 2020
|
||||
|
||||
54
README.md
54
README.md
@@ -1,8 +1,10 @@
|
||||
# OSXPhotos
|
||||
|
||||
[](https://github.com/python/black)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
- [OSXPhotos](#osxphotos)
|
||||
* [What is osxphotos?](#what-is-osxphotos)
|
||||
@@ -327,6 +329,11 @@ Options:
|
||||
exiftool may be installed from
|
||||
https://exiftool.org/. Cannot be used with
|
||||
--export-as-hardlink.
|
||||
--ignore-date-modified If used with --exiftool or --sidecar, will
|
||||
ignore the photo modification date and set
|
||||
EXIF:ModifyDate to EXIF:DateTimeOriginal;
|
||||
this is consistent with how Photos handles
|
||||
the EXIF:ModifyDate tag.
|
||||
--directory DIRECTORY Optional template for specifying name of
|
||||
output directory in the form
|
||||
'{name,DEFAULT}'. See below for additional
|
||||
@@ -1988,21 +1995,37 @@ If you have an interesting example that shows usage of this package, submit an i
|
||||
|
||||
Testing against "real world" Photos libraries would be especially helpful. If you discover issues in testing against your Photos libraries, please open an issue. I've done extensive testing against my own Photos library but that's a since data point and I'm certain there are issues lurking in various edge cases I haven't discovered yet.
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank-you to the following people who have contributed to improving osxphotos! If I've inadvertently left you off, please open an issue or send me a note.
|
||||
### Contributors ✨
|
||||
|
||||
- [britiscurious](https://github.com/britiscurious)
|
||||
- [Michel Wortmann](https://github.com/mwort)
|
||||
- [hshore29](https://github.com/hshore29)
|
||||
- [Pablo 'merKur' Kohan](https://github.com/PabloKohan)
|
||||
- [Jean-Yves Stervinou](https://github.com/jystervinou)
|
||||
- [Thibault Deutsch](https://github.com/dethi)
|
||||
- [grundsch](https://github.com/grundsch)
|
||||
- [Ag Primatic](https://github.com/agprimatic)
|
||||
- [Daniel M. Drucker](https://github.com/dmd)
|
||||
- [Horst Höck](https://github.com/hhoeck)
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/britiscurious"><img src="https://avatars1.githubusercontent.com/u/25646439?v=4?s=100" width="100px;" alt=""/><br /><sub><b>britiscurious</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mwort"><img src="https://avatars3.githubusercontent.com/u/8170417?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michel Wortmann</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mwort" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/PabloKohan"><img src="https://avatars3.githubusercontent.com/u/8790976?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pablo 'merKur' Kohan</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=PabloKohan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hshore29"><img src="https://avatars2.githubusercontent.com/u/7023497?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hshore29</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hshore29" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://3e.org/"><img src="https://avatars0.githubusercontent.com/u/41439?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel M. Drucker</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dmd" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jystervinou"><img src="https://avatars3.githubusercontent.com/u/132356?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jean-Yves Stervinou</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jystervinou" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://dethi.me/"><img src="https://avatars2.githubusercontent.com/u/1011520?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thibault Deutsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dethi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
## Known Bugs
|
||||
|
||||
@@ -2035,5 +2058,4 @@ For additional details about how osxphotos is implemented or if you would like t
|
||||
## Acknowledgements
|
||||
This project was originally inspired by [photo-export](https://github.com/patrikhson/photo-export) by Patrick Fältström, Copyright (c) 2015 Patrik Fältström paf@frobbit.se
|
||||
|
||||
I use [py-applescript](https://github.com/rdhyee/py-applescript) by "Raymond Yee / rdhyee" to interact with Photos. Rather than import this package, I included the entire package (which is published as public domain code) in a private package to prevent ambiguity with other applescript packages on PyPi. py-applescript uses a native bridge via PyObjC and is very fast compared to the other osascript based packages.
|
||||
|
||||
I use [py-applescript](https://github.com/rdhyee/py-applescript) by "Raymond Yee / rdhyee" to interact with Photos. Rather than import this package, I included the entire package (which is published as public domain code) in a private package to prevent ambiguity with other applescript packages on PyPi. py-applescript uses a native bridge via PyObjC and is very fast compared to the other osascript based packages.
|
||||
@@ -1290,6 +1290,13 @@ def query(
|
||||
"exiftool may be installed from https://exiftool.org/. "
|
||||
"Cannot be used with --export-as-hardlink.",
|
||||
)
|
||||
@click.option(
|
||||
"--ignore-date-modified",
|
||||
is_flag=True,
|
||||
help="If used with --exiftool or --sidecar, will ignore the photo "
|
||||
"modification date and set EXIF:ModifyDate to EXIF:DateTimeOriginal; "
|
||||
"this is consistent with how Photos handles the EXIF:ModifyDate tag.",
|
||||
)
|
||||
@click.option(
|
||||
"--directory",
|
||||
metavar="DIRECTORY",
|
||||
@@ -1389,6 +1396,7 @@ def export(
|
||||
download_missing,
|
||||
dest,
|
||||
exiftool,
|
||||
ignore_date_modified,
|
||||
portrait,
|
||||
not_portrait,
|
||||
screenshot,
|
||||
@@ -1663,6 +1671,7 @@ def export(
|
||||
use_photos_export=use_photos_export,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
results_exported.extend(results.exported)
|
||||
results_new.extend(results.new)
|
||||
@@ -1712,6 +1721,7 @@ def export(
|
||||
use_photos_export=use_photos_export,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
results_exported.extend(results.exported)
|
||||
results_new.extend(results.new)
|
||||
@@ -2218,6 +2228,7 @@ def export_photo(
|
||||
use_photos_export=False,
|
||||
convert_to_jpeg=False,
|
||||
jpeg_quality=1.0,
|
||||
ignore_date_modified=False,
|
||||
):
|
||||
""" Helper function for export that does the actual export
|
||||
|
||||
@@ -2251,6 +2262,7 @@ def export_photo(
|
||||
use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing
|
||||
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
||||
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||
|
||||
Returns:
|
||||
list of path(s) of exported photo or None if photo was missing
|
||||
@@ -2305,10 +2317,13 @@ def export_photo(
|
||||
|
||||
# if download_missing and the photo is missing or path doesn't exist,
|
||||
# try to download with Photos
|
||||
use_photos_export = (
|
||||
download_missing and (photo.ismissing or not os.path.exists(photo.path))
|
||||
if not use_photos_export
|
||||
else True
|
||||
use_photos_export = use_photos_export or (
|
||||
download_missing
|
||||
and (
|
||||
photo.ismissing
|
||||
or not os.path.exists(photo.path)
|
||||
or (export_edited and photo.path_edited is None)
|
||||
)
|
||||
)
|
||||
|
||||
# export the photo to each path in dest_paths
|
||||
@@ -2339,6 +2354,7 @@ def export_photo(
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
|
||||
results_exported.extend(export_results.exported)
|
||||
@@ -2400,6 +2416,7 @@ def export_photo(
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
|
||||
results_exported.extend(export_results_edited.exported)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.36.2"
|
||||
__version__ = "0.36.6"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
_export_photo
|
||||
_write_exif_data
|
||||
_exiftool_json_sidecar
|
||||
_exiftool_dict
|
||||
_xmp_sidecar
|
||||
_write_sidecar
|
||||
"""
|
||||
@@ -308,6 +309,7 @@ def export2(
|
||||
touch_file=False,
|
||||
convert_to_jpeg=False,
|
||||
jpeg_quality=1.0,
|
||||
ignore_date_modified=False,
|
||||
):
|
||||
""" export photo, like export but with update and dry_run options
|
||||
dest: must be valid destination path or exception raised
|
||||
@@ -350,6 +352,7 @@ def export2(
|
||||
touch_file: (boolean, default=False); if True, sets file's modification time upon photo date
|
||||
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
||||
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
||||
ignore_date_modified: for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||
|
||||
Returns: ExportResults namedtuple with fields: exported, new, updated, skipped
|
||||
where each field is a list of file paths
|
||||
@@ -698,6 +701,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
if not dry_run:
|
||||
try:
|
||||
@@ -743,6 +747,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
)[0]
|
||||
if old_data != current_data:
|
||||
@@ -758,6 +763,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
export_db.set_exifdata_for_file(
|
||||
exported_file,
|
||||
@@ -766,6 +772,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
),
|
||||
)
|
||||
export_db.set_stat_exif_for_file(
|
||||
@@ -781,6 +788,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
|
||||
export_db.set_exifdata_for_file(
|
||||
@@ -790,6 +798,7 @@ def export2(
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
),
|
||||
)
|
||||
export_db.set_stat_exif_for_file(
|
||||
@@ -997,21 +1006,30 @@ def _write_exif_data(
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
ignore_date_modified=False,
|
||||
):
|
||||
""" write exif data to image file at filepath
|
||||
filepath: full path to the image file """
|
||||
|
||||
Args:
|
||||
filepath: full path to the image file
|
||||
use_albums_as_keywords: treat album names as keywords
|
||||
use_persons_as_keywords: treat person names 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
|
||||
"""
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"Could not find file {filepath}")
|
||||
exiftool = ExifTool(filepath)
|
||||
exif_info = json.loads(
|
||||
self._exiftool_json_sidecar(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
)[0]
|
||||
exif_info = self._exiftool_dict(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
for exiftag, val in exif_info.items():
|
||||
if exiftag == "_CreatedBy":
|
||||
continue
|
||||
if type(val) == list:
|
||||
# more than one, set first value the add additional values
|
||||
exiftool.setvalue(exiftag, val.pop(0))
|
||||
@@ -1022,37 +1040,46 @@ def _write_exif_data(
|
||||
exiftool.setvalue(exiftag, val)
|
||||
|
||||
|
||||
def _exiftool_json_sidecar(
|
||||
def _exiftool_dict(
|
||||
self,
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
ignore_date_modified=False,
|
||||
):
|
||||
""" return json string of EXIF details in exiftool sidecar format
|
||||
Does not include all the EXIF fields as those are likely already in the image
|
||||
""" 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.
|
||||
|
||||
Args:
|
||||
use_albums_as_keywords: treat album names as keywords
|
||||
use_persons_as_keywords: treat person names as keywords
|
||||
keyword_template: (list of strings); list of template strings to render as keywords
|
||||
Exports the following:
|
||||
FileName
|
||||
ImageDescription
|
||||
Description
|
||||
Title
|
||||
TagsList
|
||||
Keywords (may include album name, person name, or template)
|
||||
Subject
|
||||
PersonInImage
|
||||
GPSLatitude, GPSLongitude
|
||||
GPSPosition
|
||||
GPSLatitudeRef, GPSLongitudeRef
|
||||
DateTimeOriginal
|
||||
OffsetTimeOriginal
|
||||
ModifyDate """
|
||||
description_template: (list of strings); list of template strings to render for the description
|
||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||
|
||||
Returns: dict with exiftool tags / values
|
||||
|
||||
Exports the following:
|
||||
EXIF:ImageDescription
|
||||
XMP:Description (may include template)
|
||||
XMP:Title
|
||||
XMP:TagsList
|
||||
IPTC:Keywords (may include album name, person name, or template)
|
||||
XMP:Subject
|
||||
XMP:PersonInImage
|
||||
EXIF:GPSLatitude, EXIF:GPSLongitude
|
||||
EXIF:GPSPosition
|
||||
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
||||
EXIF:DateTimeOriginal
|
||||
EXIF:OffsetTimeOriginal
|
||||
EXIF:ModifyDate
|
||||
IPTC:DigitalCreationDate
|
||||
IPTC:DateCreated
|
||||
"""
|
||||
|
||||
exif = {}
|
||||
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
if description_template is not None:
|
||||
description = self.render_template(
|
||||
description_template, expand_inplace=True, inplace_sep=", "
|
||||
@@ -1114,15 +1141,16 @@ def _exiftool_json_sidecar(
|
||||
keyword_list.extend(rendered_keywords)
|
||||
|
||||
if keyword_list:
|
||||
exif["XMP:TagsList"] = exif["IPTC:Keywords"] = keyword_list
|
||||
exif["XMP:TagsList"] = keyword_list.copy()
|
||||
exif["IPTC:Keywords"] = keyword_list.copy()
|
||||
|
||||
if person_list:
|
||||
exif["XMP:PersonInImage"] = person_list
|
||||
exif["XMP:PersonInImage"] = person_list.copy()
|
||||
|
||||
if self.keywords or person_list:
|
||||
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
|
||||
# only use Photos' keywords for subject
|
||||
exif["XMP:Subject"] = list(self.keywords) + person_list
|
||||
# only use Photos' keywords for subject (e.g. don't include template values)
|
||||
exif["XMP:Subject"] = self.keywords.copy() + person_list.copy()
|
||||
|
||||
# if self.favorite():
|
||||
# exif["Rating"] = 5
|
||||
@@ -1137,22 +1165,86 @@ def _exiftool_json_sidecar(
|
||||
exif["EXIF:GPSLongitudeRef"] = lon_ref
|
||||
|
||||
# process date/time and timezone offset
|
||||
# Photos exports the following fields and sets modify date to creation date
|
||||
# [EXIF] Modify Date : 2020:10:30 00:00:00
|
||||
# [EXIF] Date/Time Original : 2020:10:30 00:00:00
|
||||
# [EXIF] Create Date : 2020:10:30 00:00:00
|
||||
# [IPTC] Digital Creation Date : 2020:10:30
|
||||
# [IPTC] Date Created : 2020:10:30
|
||||
#
|
||||
# This code deviates from Photos in one regard:
|
||||
# if photo has modification date, use it otherwise use creation date
|
||||
date = self.date
|
||||
|
||||
# exiftool expects format to "2015:01:18 12:00:00"
|
||||
datetimeoriginal = date.strftime("%Y:%m:%d %H:%M:%S")
|
||||
exif["EXIF:DateTimeOriginal"] = datetimeoriginal
|
||||
exif["EXIF:CreateDate"] = datetimeoriginal
|
||||
|
||||
offsettime = date.strftime("%z")
|
||||
# find timezone offset in format "-04:00"
|
||||
offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
|
||||
offset = offset[0] # findall returns list of tuples
|
||||
offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
|
||||
exif["EXIF:DateTimeOriginal"] = datetimeoriginal
|
||||
exif["EXIF:OffsetTimeOriginal"] = offsettime
|
||||
|
||||
if self.date_modified is not None:
|
||||
exif["EXIF:ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
|
||||
dateoriginal = date.strftime("%Y:%m:%d")
|
||||
exif["IPTC:DigitalCreationDate"] = dateoriginal
|
||||
exif["IPTC:DateCreated"] = dateoriginal
|
||||
|
||||
json_str = json.dumps([exif])
|
||||
return json_str
|
||||
if self.date_modified is not None and not ignore_date_modified:
|
||||
exif["EXIF:ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
|
||||
else:
|
||||
exif["EXIF:ModifyDate"] = self.date.strftime("%Y:%m:%d %H:%M:%S")
|
||||
|
||||
return exif
|
||||
|
||||
|
||||
def _exiftool_json_sidecar(
|
||||
self,
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
ignore_date_modified=False,
|
||||
):
|
||||
""" 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.
|
||||
|
||||
Args:
|
||||
use_albums_as_keywords: treat album names as keywords
|
||||
use_persons_as_keywords: treat person names as keywords
|
||||
keyword_template: (list of strings); list of template strings to render as keywords
|
||||
description_template: (list of strings); list of template strings to render for the description
|
||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||
|
||||
Returns: dict with exiftool tags / values
|
||||
|
||||
Exports the following:
|
||||
EXIF:ImageDescription
|
||||
XMP:Description (may include template)
|
||||
XMP:Title
|
||||
XMP:TagsList
|
||||
IPTC:Keywords (may include album name, person name, or template)
|
||||
XMP:Subject
|
||||
XMP:PersonInImage
|
||||
EXIF:GPSLatitude, EXIF:GPSLongitude
|
||||
EXIF:GPSPosition
|
||||
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
||||
EXIF:DateTimeOriginal
|
||||
EXIF:OffsetTimeOriginal
|
||||
EXIF:ModifyDate
|
||||
IPTC:DigitalCreationDate
|
||||
IPTC:DateCreated
|
||||
"""
|
||||
exif = self._exiftool_dict(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
)
|
||||
return json.dumps([exif])
|
||||
|
||||
|
||||
def _xmp_sidecar(
|
||||
|
||||
@@ -53,6 +53,7 @@ class PhotoInfo:
|
||||
export,
|
||||
export2,
|
||||
_export_photo,
|
||||
_exiftool_dict,
|
||||
_exiftool_json_sidecar,
|
||||
_write_exif_data,
|
||||
_write_sidecar,
|
||||
@@ -60,7 +61,7 @@ class PhotoInfo:
|
||||
ExportResults,
|
||||
)
|
||||
from ._photoinfo_scoreinfo import score, ScoreInfo
|
||||
from ._photoinfo_comments import comments, likes
|
||||
from ._photoinfo_comments import comments, likes
|
||||
|
||||
def __init__(self, db=None, uuid=None, info=None):
|
||||
self._uuid = uuid
|
||||
@@ -70,7 +71,11 @@ class PhotoInfo:
|
||||
@property
|
||||
def filename(self):
|
||||
""" filename of the picture """
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self.has_raw and self.raw_original:
|
||||
if (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
and self.raw_original
|
||||
):
|
||||
# return the JPEG version as that's what Photos 5+ does
|
||||
return self._info["raw_pair_info"]["filename"]
|
||||
else:
|
||||
@@ -80,7 +85,11 @@ class PhotoInfo:
|
||||
def original_filename(self):
|
||||
""" original filename of the picture
|
||||
Photos 5 mangles filenames upon import """
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self.has_raw and self.raw_original:
|
||||
if (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
and self.raw_original
|
||||
):
|
||||
# return the JPEG version as that's what Photos 5+ does
|
||||
return self._info["raw_pair_info"]["originalFilename"]
|
||||
else:
|
||||
@@ -95,12 +104,20 @@ class PhotoInfo:
|
||||
def date_modified(self):
|
||||
""" image modification date as timezone aware datetime object
|
||||
or None if no modification date set """
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
|
||||
# Photos <= 4 provides no way to get date of adjustment and will update
|
||||
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
|
||||
# only report lastmodified date for Photos <=4 if photo is edited;
|
||||
# even in this case, the date could be incorrect
|
||||
if self.hasadjustments or self._db._db_version > _PHOTOS_4_VERSION:
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -179,18 +196,15 @@ class PhotoInfo:
|
||||
""" absolute path on disk of the edited picture """
|
||||
""" None if photo has not been edited """
|
||||
|
||||
# TODO: break this code into a _path_edited_4 and _path_edited_5
|
||||
# version to simplify the big if/then; same for path_live_photo
|
||||
|
||||
try:
|
||||
return self._path_edited
|
||||
except AttributeError:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
self._path_edited = self._path_edited_4()
|
||||
return self._path_edited
|
||||
else:
|
||||
self._path_edited = self._path_edited_5()
|
||||
return self._path_edited
|
||||
|
||||
return self._path_edited
|
||||
|
||||
def _path_edited_5(self):
|
||||
""" return path_edited for Photos >= 5 """
|
||||
@@ -248,8 +262,6 @@ class PhotoInfo:
|
||||
# if self._info["isMissing"] == 1:
|
||||
# photopath = None # path would be meaningless until downloaded
|
||||
|
||||
# logging.debug(photopath)
|
||||
|
||||
return photopath
|
||||
|
||||
def _path_edited_4(self):
|
||||
@@ -282,7 +294,7 @@ class PhotoInfo:
|
||||
# could be elsewhere--I haven't figured out this logic yet
|
||||
# first see if it's in 00
|
||||
photopath = os.path.join(
|
||||
library, "resources", "media", "version", folder_id, "00", filename,
|
||||
library, "resources", "media", "version", folder_id, "00", filename
|
||||
)
|
||||
|
||||
if not os.path.isfile(photopath):
|
||||
@@ -841,7 +853,7 @@ class PhotoInfo:
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement
|
||||
replacement=replacement,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1026,6 +1038,7 @@ class PhotoInfo:
|
||||
|
||||
def json(self):
|
||||
""" Return JSON representation """
|
||||
|
||||
def default(o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
Binary file not shown.
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-10-09T16:14:42Z</date>
|
||||
<date>2020-11-01T02:34:49Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-10-10T05:21:03Z</date>
|
||||
<date>2020-11-01T02:34:49Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2020-10-04T23:43:17Z</date>
|
||||
<date>2020-11-01T02:34:46Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<key>SnapshotCompletedDate</key>
|
||||
<date>2019-07-27T13:16:43Z</date>
|
||||
<key>SnapshotLastValidated</key>
|
||||
<date>2020-10-10T05:22:36Z</date>
|
||||
<date>2020-11-01T02:34:46Z</date>
|
||||
<key>SnapshotTables</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
|
||||
@@ -324,6 +324,24 @@ CLI_EXIFTOOL = {
|
||||
}
|
||||
}
|
||||
|
||||
CLI_EXIFTOOL_IGNORE_DATE_MODIFIED = {
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
|
||||
"File:FileName": "wedding.jpg",
|
||||
"EXIF:ImageDescription": "Bride Wedding day",
|
||||
"XMP:Description": "Bride Wedding day",
|
||||
"XMP:TagsList": "wedding",
|
||||
"IPTC:Keywords": "wedding",
|
||||
"XMP:PersonInImage": "Maria",
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"EXIF:DateTimeOriginal": "2019:04:15 14:40:24",
|
||||
"EXIF:CreateDate": "2019:04:15 14:40:24",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2019:04:15",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
"EXIF:ModifyDate": "2019:04:15 14:40:24",
|
||||
}
|
||||
}
|
||||
|
||||
LABELS_JSON = {
|
||||
"labels": {
|
||||
"Plant": 7,
|
||||
@@ -931,6 +949,38 @@ def test_export_exiftool():
|
||||
assert exif[key] == CLI_EXIFTOOL[uuid][key]
|
||||
|
||||
|
||||
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||
def test_export_exiftool_ignore_date_modified():
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import export
|
||||
from osxphotos.exiftool import ExifTool
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
for uuid in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED:
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_6),
|
||||
".",
|
||||
"-V",
|
||||
"--exiftool",
|
||||
"--ignore-date-modified",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
exif = ExifTool(CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]["File:FileName"]).asdict()
|
||||
for key in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]:
|
||||
assert exif[key] == CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key]
|
||||
|
||||
|
||||
def test_export_edited_suffix():
|
||||
""" test export with --edited-suffix """
|
||||
import glob
|
||||
@@ -2632,16 +2682,21 @@ def test_export_sidecar_keyword_template():
|
||||
|
||||
json_expected = json.loads(
|
||||
"""
|
||||
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
|
||||
"EXIF:ImageDescription": "Girl holding pumpkin",
|
||||
"XMP:Description": "Girl holding pumpkin",
|
||||
"XMP:Title": "I found one!",
|
||||
"XMP:TagsList": ["Kids", "Multi Keyword", "Test Album", "Pumpkin Farm"],
|
||||
"IPTC:Keywords": ["Kids", "Multi Keyword", "Test Album", "Pumpkin Farm"],
|
||||
"XMP:PersonInImage": ["Katie"],
|
||||
"XMP:Subject": ["Kids", "Katie"],
|
||||
"EXIF:DateTimeOriginal": "2018:09:28 16:07:07",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00"}]"""
|
||||
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
|
||||
"EXIF:ImageDescription": "Girl holding pumpkin",
|
||||
"XMP:Description": "Girl holding pumpkin",
|
||||
"XMP:Title": "I found one!",
|
||||
"XMP:TagsList": ["Kids", "Multi Keyword", "Pumpkin Farm", "Test Album"],
|
||||
"IPTC:Keywords": ["Kids", "Multi Keyword", "Pumpkin Farm", "Test Album"],
|
||||
"XMP:PersonInImage": ["Katie"],
|
||||
"XMP:Subject": ["Kids", "Katie"],
|
||||
"EXIF:DateTimeOriginal": "2018:09:28 16:07:07",
|
||||
"EXIF:CreateDate": "2018:09:28 16:07:07",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2018:09:28",
|
||||
"IPTC:DateCreated": "2018:09:28",
|
||||
"EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||
"""
|
||||
)[0]
|
||||
|
||||
with open("Pumkins2.jpg.json", "r") as json_file:
|
||||
|
||||
@@ -67,18 +67,37 @@ XMP_FILENAME = "Pumkins1.jpg.xmp"
|
||||
XMP_JPG_FILENAME = "Pumkins1.jpg"
|
||||
|
||||
EXIF_JSON_UUID = UUID_DICT["has_adjustments"]
|
||||
EXIF_JSON_EXPECTED = (
|
||||
'[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", '
|
||||
'"EXIF:ImageDescription": "Bride Wedding day", '
|
||||
'"XMP:Description": "Bride Wedding day", '
|
||||
'"XMP:TagsList": ["wedding"], '
|
||||
'"IPTC:Keywords": ["wedding"], '
|
||||
'"XMP:PersonInImage": ["Maria"], '
|
||||
'"XMP:Subject": ["wedding", "Maria"], '
|
||||
'"EXIF:DateTimeOriginal": "2019:04:15 14:40:24", '
|
||||
'"EXIF:OffsetTimeOriginal": "-04:00", '
|
||||
'"EXIF:ModifyDate": "2019:07:27 17:33:28"}]'
|
||||
)
|
||||
EXIF_JSON_EXPECTED = """
|
||||
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
|
||||
"EXIF:ImageDescription": "Bride Wedding day",
|
||||
"XMP:Description": "Bride Wedding day",
|
||||
"XMP:TagsList": ["wedding"],
|
||||
"IPTC:Keywords": ["wedding"],
|
||||
"XMP:PersonInImage": ["Maria"],
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"EXIF:DateTimeOriginal": "2019:04:15 14:40:24",
|
||||
"EXIF:CreateDate": "2019:04:15 14:40:24",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2019:04:15",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
"EXIF:ModifyDate": "2019:07:27 17:33:28"}]
|
||||
"""
|
||||
|
||||
EXIF_JSON_EXPECTED_IGNORE_DATE_MODIFIED = """
|
||||
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
|
||||
"EXIF:ImageDescription": "Bride Wedding day",
|
||||
"XMP:Description": "Bride Wedding day",
|
||||
"XMP:TagsList": ["wedding"],
|
||||
"IPTC:Keywords": ["wedding"],
|
||||
"XMP:PersonInImage": ["Maria"],
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"EXIF:DateTimeOriginal": "2019:04:15 14:40:24",
|
||||
"EXIF:CreateDate": "2019:04:15 14:40:24",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2019:04:15",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
"EXIF:ModifyDate": "2019:04:15 14:40:24"}]
|
||||
"""
|
||||
|
||||
|
||||
def test_export_1():
|
||||
@@ -489,6 +508,32 @@ def test_exiftool_json_sidecar():
|
||||
assert json_got[k] == v
|
||||
|
||||
|
||||
def test_exiftool_json_sidecar_ignore_date_modified():
|
||||
import osxphotos
|
||||
import json
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[EXIF_JSON_UUID])
|
||||
|
||||
json_expected = json.loads(EXIF_JSON_EXPECTED_IGNORE_DATE_MODIFIED)[0]
|
||||
|
||||
json_got = photos[0]._exiftool_json_sidecar(ignore_date_modified=True)
|
||||
json_got = json.loads(json_got)[0]
|
||||
|
||||
# some gymnastics to account for different sort order in different pythons
|
||||
for k, v in json_got.items():
|
||||
if type(v) in (list, tuple):
|
||||
assert sorted(json_expected[k]) == sorted(v)
|
||||
else:
|
||||
assert json_expected[k] == v
|
||||
|
||||
for k, v in json_expected.items():
|
||||
if type(v) in (list, tuple):
|
||||
assert sorted(json_got[k]) == sorted(v)
|
||||
else:
|
||||
assert json_got[k] == v
|
||||
|
||||
|
||||
def test_exiftool_json_sidecar_keyword_template_long(caplog):
|
||||
import osxphotos
|
||||
from osxphotos._constants import _MAX_IPTC_KEYWORD_LEN
|
||||
@@ -507,7 +552,10 @@ def test_exiftool_json_sidecar_keyword_template_long(caplog):
|
||||
"XMP:PersonInImage": ["Maria"],
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"EXIF:DateTimeOriginal": "2019:04:15 14:40:24",
|
||||
"EXIF:CreateDate": "2019:04:15 14:40:24",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2019:04:15",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
"EXIF:ModifyDate": "2019:07:27 17:33:28"}]
|
||||
"""
|
||||
)[0]
|
||||
@@ -552,9 +600,12 @@ def test_exiftool_json_sidecar_keyword_template():
|
||||
"XMP:TagsList": ["wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"],
|
||||
"IPTC:Keywords": ["wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"],
|
||||
"XMP:PersonInImage": ["Maria"],
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"XMP:Subject": ["wedding", "Maria"],
|
||||
"EXIF:DateTimeOriginal": "2019:04:15 14:40:24",
|
||||
"EXIF:CreateDate": "2019:04:15 14:40:24",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2019:04:15",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
"EXIF:ModifyDate": "2019:07:27 17:33:28"}]
|
||||
"""
|
||||
)[0]
|
||||
@@ -609,11 +660,15 @@ def test_exiftool_json_sidecar_use_persons_keyword():
|
||||
"XMP:Description": "Girls with pumpkins",
|
||||
"XMP:Title": "Can we carry this?",
|
||||
"XMP:TagsList": ["Kids", "Suzy", "Katie"],
|
||||
"IPTC:Keywords": ["Kids", "Suzy", "Katie"],
|
||||
"XMP:PersonInImage": ["Suzy", "Katie"],
|
||||
"XMP:Subject": ["Kids", "Suzy", "Katie"],
|
||||
"IPTC:Keywords": ["Kids", "Suzy", "Katie"],
|
||||
"XMP:PersonInImage": ["Suzy", "Katie"],
|
||||
"XMP:Subject": ["Kids", "Suzy", "Katie"],
|
||||
"EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00"}]
|
||||
"EXIF:CreateDate": "2018:09:28 15:35:49",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2018:09:28",
|
||||
"IPTC:DateCreated": "2018:09:28",
|
||||
"EXIF:ModifyDate": "2018:09:28 15:35:49"}]
|
||||
"""
|
||||
)[0]
|
||||
|
||||
@@ -652,7 +707,11 @@ def test_exiftool_json_sidecar_use_albums_keyword():
|
||||
"XMP:PersonInImage": ["Suzy", "Katie"],
|
||||
"XMP:Subject": ["Kids", "Suzy", "Katie"],
|
||||
"EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00"}]
|
||||
"EXIF:CreateDate": "2018:09:28 15:35:49",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
"IPTC:DigitalCreationDate": "2018:09:28",
|
||||
"IPTC:DateCreated": "2018:09:28",
|
||||
"EXIF:ModifyDate": "2018:09:28 15:35:49"}]
|
||||
"""
|
||||
)[0]
|
||||
|
||||
|
||||
@@ -45,17 +45,23 @@ UUID_DICT = {
|
||||
"xmp": "8SOE9s0XQVGsuq4ONohTng",
|
||||
}
|
||||
|
||||
EXIF_JSON_EXPECTED = (
|
||||
'[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", '
|
||||
'"XMP:Title": "St. James\'s Park", "XMP:TagsList": ["UK", "England", '
|
||||
'"London", "United Kingdom", "London 2018", "St. James\'s Park"], '
|
||||
'"IPTC:Keywords": ["UK", "England", "London", "United Kingdom", "London 2018", '
|
||||
'"St. James\'s Park"], "XMP:Subject": ["UK", "England", "London", "United Kingdom", '
|
||||
'"London 2018", "St. James\'s Park"], "EXIF:GPSLatitude": 51.50357167, '
|
||||
'"EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", '
|
||||
'"EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", '
|
||||
'"EXIF:OffsetTimeOriginal": "-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]'
|
||||
)
|
||||
EXIF_JSON_EXPECTED = """
|
||||
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
|
||||
"XMP:Title": "St. James\'s Park",
|
||||
"XMP:TagsList": ["UK", "England", "London", "United Kingdom", "London 2018", "St. James\'s Park"],
|
||||
"IPTC:Keywords": ["UK", "England", "London", "United Kingdom", "London 2018", "St. James\'s Park"],
|
||||
"XMP:Subject": ["UK", "England", "London", "United Kingdom", "London 2018", "St. James\'s Park"],
|
||||
"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:DigitalCreationDate": "2018:10:13",
|
||||
"IPTC:DateCreated": "2018:10:13",
|
||||
"EXIF:ModifyDate": "2019:12:01 11:43:45"}]
|
||||
"""
|
||||
|
||||
|
||||
def test_export_1():
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
""" Test basic methods for Mojave 10.14.6 """
|
||||
|
||||
import datetime
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
|
||||
from collections import namedtuple
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
@@ -535,6 +539,31 @@ def test_date_modified_invalid(photosdb):
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
def test_date_modified(photosdb):
|
||||
""" Test date modified for photo that has been edited """
|
||||
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
p = photos[0]
|
||||
assert p.date_modified == datetime.datetime(
|
||||
2019,
|
||||
11,
|
||||
27,
|
||||
1,
|
||||
30,
|
||||
16,
|
||||
681150,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)),
|
||||
)
|
||||
|
||||
|
||||
def test_date_modified_none(photosdb):
|
||||
""" Test date modified for a photo that hasn't been edited """
|
||||
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
p = photos[0]
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
def test_uti(photosdb):
|
||||
for uuid, utis in UUID_UTI_DICT.items():
|
||||
photo = photosdb.get_photo(uuid)
|
||||
|
||||
Reference in New Issue
Block a user