Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b23cfa32bb | ||
|
|
0e22ce54ab | ||
|
|
0f41588701 | ||
|
|
442b542794 | ||
|
|
88fae81b19 | ||
|
|
c4fec00f67 | ||
|
|
9a0cc3e8fa | ||
|
|
3ed2362fe3 | ||
|
|
cd8dd552a4 | ||
|
|
64379f313e | ||
|
|
dc53480126 | ||
|
|
3e06e0e344 | ||
|
|
aa9f6520d4 | ||
|
|
3a56e05c85 | ||
|
|
454a813908 | ||
|
|
31e162ba94 | ||
|
|
5b8d51da38 | ||
|
|
846ea89012 | ||
|
|
dc0bbd5fd6 | ||
|
|
91804d53ea | ||
|
|
3d26206d91 | ||
|
|
92d9dfaef2 | ||
|
|
51025e7f8b | ||
|
|
d52e5e9316 | ||
|
|
3711b3f7f1 | ||
|
|
48c229b52c | ||
|
|
aad435da36 | ||
|
|
9c60259089 | ||
|
|
131105d82c | ||
|
|
f54205ff49 | ||
|
|
1d14fc8041 | ||
|
|
4aec01ad1d | ||
|
|
3630643a0e | ||
|
|
44966c6736 | ||
|
|
7c4b28a35c | ||
|
|
1cdf4addad | ||
|
|
50fa851f23 | ||
|
|
a483b8a900 | ||
|
|
dd6d519135 | ||
|
|
9371db094e | ||
|
|
d9a82f29c7 | ||
|
|
3f57514fa3 | ||
|
|
a5a155bd05 | ||
|
|
c8ea0b0452 | ||
|
|
81fd51c793 | ||
|
|
648d399524 | ||
|
|
345c052353 | ||
|
|
952f1a6c3c | ||
|
|
7ae5b8aae7 | ||
|
|
2e189d771e | ||
|
|
7fa7de1563 | ||
|
|
70d68a25ba | ||
|
|
bfc4371d9e | ||
|
|
6a288676a1 | ||
|
|
874ad2fa34 | ||
|
|
a233167471 | ||
|
|
21dc0d388f | ||
|
|
eff8e7a63f | ||
|
|
03f8b2bc6e | ||
|
|
e215c200c7 | ||
|
|
ae5b02f563 | ||
|
|
aa1a96d201 | ||
|
|
d9f24307ac | ||
|
|
958f8c343a | ||
|
|
70cf4c9f92 | ||
|
|
2d3344ee34 | ||
|
|
b4bc906b6a | ||
|
|
520a15fac6 | ||
|
|
032dff8967 | ||
|
|
3c36b0fb33 | ||
|
|
d51d7a41e4 | ||
|
|
60c926fea5 | ||
|
|
db27aac14b | ||
|
|
d17454772c | ||
|
|
9c9e73ba96 | ||
|
|
e21a78c2b3 | ||
|
|
de0fbf2bb9 | ||
|
|
b330e27fb8 | ||
|
|
a941f66d62 | ||
|
|
d77eba12b2 | ||
|
|
de94fd76de |
@@ -193,6 +193,26 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ubrandes",
|
||||
"name": "ubrandes ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59647284?v=4",
|
||||
"profile": "https://github.com/ubrandes",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pdewost",
|
||||
"name": "Philippe Dewost",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17090228?v=4",
|
||||
"profile": "http://blog.dewost.com/",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"example",
|
||||
"ideas"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__
|
||||
t.out
|
||||
.vscode/
|
||||
.tox/
|
||||
.idea/
|
||||
dist/
|
||||
build/
|
||||
working/
|
||||
|
||||
151
CHANGELOG.md
151
CHANGELOG.md
@@ -4,6 +4,157 @@ 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.42.15](https://github.com/RhetTbull/osxphotos/compare/v0.42.14...v0.42.15)
|
||||
|
||||
> 2 May 2021
|
||||
|
||||
- Added --add-to-album to query [`9a0cc3e`](https://github.com/RhetTbull/osxphotos/commit/9a0cc3e8fa024b485010dbe47791435acf0f7163)
|
||||
- Updated docs [skip ci] [`c4fec00`](https://github.com/RhetTbull/osxphotos/commit/c4fec00f6711fa1422c39ec61384ba39418085ff)
|
||||
|
||||
#### [v0.42.14](https://github.com/RhetTbull/osxphotos/compare/v0.42.13...v0.42.14)
|
||||
|
||||
> 2 May 2021
|
||||
|
||||
- Add --add-exported-to-album, # 428 [`cd8dd55`](https://github.com/RhetTbull/osxphotos/commit/cd8dd552a479905348555f5a17d2ad3a3f25ac69)
|
||||
- Updated tutorial [`3e06e0e`](https://github.com/RhetTbull/osxphotos/commit/3e06e0e344595fbb084503c8202bc04229e4289b)
|
||||
- Updated tutorial [`aa9f652`](https://github.com/RhetTbull/osxphotos/commit/aa9f6520d4d3959dfe98b465a082f6d57cc3a70d)
|
||||
|
||||
#### [v0.42.13](https://github.com/RhetTbull/osxphotos/compare/v0.42.12...v0.42.13)
|
||||
|
||||
> 26 April 2021
|
||||
|
||||
- Added read-only ExifToolCaching class, to implement #325 [`91804d5`](https://github.com/RhetTbull/osxphotos/commit/91804d53eaafddb7bff83b065014099827bb9d43)
|
||||
- Added normalized flag to ExifTool.asdict() [`3d26206`](https://github.com/RhetTbull/osxphotos/commit/3d26206d91d51a0a57670212b25afa4786953c02)
|
||||
- Updated docs [`dc0bbd5`](https://github.com/RhetTbull/osxphotos/commit/dc0bbd5fd689f38d19cae16fce0a6ee11819ca08)
|
||||
|
||||
#### [v0.42.12](https://github.com/RhetTbull/osxphotos/compare/v0.42.11...v0.42.12)
|
||||
|
||||
> 25 April 2021
|
||||
|
||||
- Added {edited_version} template field, closes #420 [`#420`](https://github.com/RhetTbull/osxphotos/issues/420)
|
||||
|
||||
#### [v0.42.11](https://github.com/RhetTbull/osxphotos/compare/v0.42.9...v0.42.11)
|
||||
|
||||
> 25 April 2021
|
||||
|
||||
- Bump py from 1.8.0 to 1.10.0 [`#434`](https://github.com/RhetTbull/osxphotos/pull/434)
|
||||
- Fixed handling of burst image selected/key/default, closes #401 (again) [`#401`](https://github.com/RhetTbull/osxphotos/issues/401)
|
||||
- Added tutorial to README [`f54205f`](https://github.com/RhetTbull/osxphotos/commit/f54205ff49a37bbef4dfca435602a50fbb4ebd02)
|
||||
- Refactored export_photo to enable work on #420 [`48c229b`](https://github.com/RhetTbull/osxphotos/commit/48c229b52c9a1881832d61434fcf38284ade918c)
|
||||
- Refactored README.md to improve Template System section [`1d14fc8`](https://github.com/RhetTbull/osxphotos/commit/1d14fc8041ae0a2b7db3b95bb08a5986176de649)
|
||||
- Updated tutorial [`aad435d`](https://github.com/RhetTbull/osxphotos/commit/aad435da3683834e17cb18b87c2aa7d1306e068e)
|
||||
- Fixed typo in tutorial [`131105d`](https://github.com/RhetTbull/osxphotos/commit/131105d82cf74bdf2dbf67077fd317d775c5b74e)
|
||||
|
||||
#### [v0.42.9](https://github.com/RhetTbull/osxphotos/compare/v0.42.8...v0.42.9)
|
||||
|
||||
> 21 April 2021
|
||||
|
||||
- Added --regex query option, closes #433 [`#433`](https://github.com/RhetTbull/osxphotos/issues/433)
|
||||
|
||||
#### [v0.42.8](https://github.com/RhetTbull/osxphotos/compare/v0.42.6...v0.42.8)
|
||||
|
||||
> 19 April 2021
|
||||
|
||||
- Added function filter to template system, closes #429 [`#429`](https://github.com/RhetTbull/osxphotos/issues/429)
|
||||
- Updated docs [skip ci] [`3f57514`](https://github.com/RhetTbull/osxphotos/commit/3f57514fa37bdaf372f52e02dbf76f1bc2b66b9b)
|
||||
- Updated docs [`50fa851`](https://github.com/RhetTbull/osxphotos/commit/50fa851f23f5a40f116d520fc70b1f523636b9a3)
|
||||
- Added template_filter.py to examples [`9371db0`](https://github.com/RhetTbull/osxphotos/commit/9371db094e40c3d64745b705b8b3ebdcbd04267d)
|
||||
- Fixed docs for function: filter [`1cdf4ad`](https://github.com/RhetTbull/osxphotos/commit/1cdf4addade706b5bf3105441a70fc9d529608a9)
|
||||
- Version bump [`a483b8a`](https://github.com/RhetTbull/osxphotos/commit/a483b8a900de66b6124e91d53c44260e3c3dfea8)
|
||||
|
||||
#### [v0.42.6](https://github.com/RhetTbull/osxphotos/compare/v0.42.4...v0.42.6)
|
||||
|
||||
> 18 April 2021
|
||||
|
||||
- Refactored _query to PhotosDB.query() [`345c052`](https://github.com/RhetTbull/osxphotos/commit/345c052353ee191272f98deda33a04a4d7945f1e)
|
||||
- Cleaned up queryoptions.py [`81fd51c`](https://github.com/RhetTbull/osxphotos/commit/81fd51c793c93d6bfe781eb21a8b8562b54db1cd)
|
||||
- Added re to photosdb for use with query_eval [`c8ea0b0`](https://github.com/RhetTbull/osxphotos/commit/c8ea0b0452b22154cc70813014c63c5d1d63c43c)
|
||||
|
||||
#### [v0.42.4](https://github.com/RhetTbull/osxphotos/compare/v0.42.3...v0.42.4)
|
||||
|
||||
> 18 April 2021
|
||||
|
||||
- Added --min-size, --max-size query options, #425 [`7ae5b8a`](https://github.com/RhetTbull/osxphotos/commit/7ae5b8aae78621c5b7501f9faa5e0f7f4d815ba1)
|
||||
- Updated docs, added build.sh [`2e189d7`](https://github.com/RhetTbull/osxphotos/commit/2e189d771edaf18c1ebffd558e3e84e43bff2f08)
|
||||
- Fixed setup.py [`952f1a6`](https://github.com/RhetTbull/osxphotos/commit/952f1a6c3c3f3c7a55c0a270e73a13c4da6d2375)
|
||||
|
||||
#### [v0.42.3](https://github.com/RhetTbull/osxphotos/compare/v0.42.2...v0.42.3)
|
||||
|
||||
> 17 April 2021
|
||||
|
||||
- Updated docs, closes #424 [`#424`](https://github.com/RhetTbull/osxphotos/issues/424)
|
||||
- Added {newline}, #426 [`7fa7de1`](https://github.com/RhetTbull/osxphotos/commit/7fa7de15631958a973514fe1a9c2cbf4301b6301)
|
||||
|
||||
#### [v0.42.2](https://github.com/RhetTbull/osxphotos/compare/v0.42.1...v0.42.2)
|
||||
|
||||
> 17 April 2021
|
||||
|
||||
- Fixed bug for multi-field templates and --xattr-template, #422 [`6a28867`](https://github.com/RhetTbull/osxphotos/commit/6a288676a14ce23380181d43db19128afdda7731)
|
||||
- Add @ubrandes as a contributor [`874ad2f`](https://github.com/RhetTbull/osxphotos/commit/874ad2fa34d8306c071cd479625a9aa97f6488b2)
|
||||
|
||||
#### [v0.42.1](https://github.com/RhetTbull/osxphotos/compare/v0.41.11...v0.42.1)
|
||||
|
||||
> 15 April 2021
|
||||
|
||||
- Implements conditional expressions for template system, #417 [`03f8b2b`](https://github.com/RhetTbull/osxphotos/commit/03f8b2bc6ed53d3176f9d1ac51c3e4469db3e94b)
|
||||
- Added {function} template, #419 [`21dc0d3`](https://github.com/RhetTbull/osxphotos/commit/21dc0d388f508c33526ba7510d78c71abd1151a9)
|
||||
- Added template_function.py to examples [`eff8e7a`](https://github.com/RhetTbull/osxphotos/commit/eff8e7a63ff77e80fff0ce53fe56f5a010f55ab5)
|
||||
|
||||
#### [v0.41.11](https://github.com/RhetTbull/osxphotos/compare/v0.41.10...v0.41.11)
|
||||
|
||||
> 12 April 2021
|
||||
|
||||
- Doc updates [`958f8c3`](https://github.com/RhetTbull/osxphotos/commit/958f8c343a93ba60c1182df32727143a750f7b15)
|
||||
- Added {photo} template, partial fix for issue #417 [`aa1a96d`](https://github.com/RhetTbull/osxphotos/commit/aa1a96d20118916a558b08e7f8ec87c43abf789b)
|
||||
- Added {favorite} template, partial fix for #289 [`d9f2430`](https://github.com/RhetTbull/osxphotos/commit/d9f24307acc9f3f7cfa01c5e47f161b3aa390a81)
|
||||
|
||||
#### [v0.41.10](https://github.com/RhetTbull/osxphotos/compare/v0.41.9...v0.41.10)
|
||||
|
||||
> 9 April 2021
|
||||
|
||||
- Added --query-eval, implements #280 [`b4bc906`](https://github.com/RhetTbull/osxphotos/commit/b4bc906b6a1c3444c5f5a5d9d908ab8c955c8f7e)
|
||||
|
||||
#### [v0.41.9](https://github.com/RhetTbull/osxphotos/compare/v0.41.8...v0.41.9)
|
||||
|
||||
> 5 April 2021
|
||||
|
||||
- Bug fix for #414, exiftool str replace [`032dff8`](https://github.com/RhetTbull/osxphotos/commit/032dff89677f049a234d9f498951b8b402d1b31c)
|
||||
|
||||
#### [v0.41.8](https://github.com/RhetTbull/osxphotos/compare/v0.41.7...v0.41.8)
|
||||
|
||||
> 4 April 2021
|
||||
|
||||
- Added --name to search filename, closes #249, #412 [`#249`](https://github.com/RhetTbull/osxphotos/issues/249)
|
||||
|
||||
#### [v0.41.7](https://github.com/RhetTbull/osxphotos/compare/v0.41.6...v0.41.7)
|
||||
|
||||
> 3 April 2021
|
||||
|
||||
- Bump pygments from 2.6.1 to 2.7.4 [`#408`](https://github.com/RhetTbull/osxphotos/pull/408)
|
||||
- Removed logging.debug code [`e21a78c`](https://github.com/RhetTbull/osxphotos/commit/e21a78c2b39ee82610394b447a9aa697e489c3e4)
|
||||
- Added test for #409 [`db27aac`](https://github.com/RhetTbull/osxphotos/commit/db27aac14bbaff0b2db44f8b2d41022ebcad18a7)
|
||||
- Update phototemplate.py [`d174547`](https://github.com/RhetTbull/osxphotos/commit/d17454772cebbd6edd5d8e0f04e80feecbdb2355)
|
||||
|
||||
#### [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
|
||||
|
||||
@@ -146,6 +146,11 @@ export default library using 'country name/year' as output directory (but use "N
|
||||
|
||||
``osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose``
|
||||
|
||||
find all videos larger than 200MB and add them to Photos album "Big Videos" creating the album if necessary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos query --only-movies --min-size 200MB --add-to-album "Big Videos"``
|
||||
|
||||
Example uses of the package
|
||||
---------------------------
|
||||
|
||||
|
||||
11
build.sh
Executable file
11
build.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# script to help build osxphotos release
|
||||
# this is unique to my own dev setup
|
||||
|
||||
activate osxphotos
|
||||
rm -rf dist; rm -rf build
|
||||
python3 utils/update_readme.py
|
||||
(cd docsrc && make github && make pdf)
|
||||
python3 setup.py sdist bdist_wheel
|
||||
./make_cli_exe.sh
|
||||
@@ -1,4 +1,4 @@
|
||||
# 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.
|
||||
config: c43f566654ff6a66a64cd55da2e67fef
|
||||
config: 452d077d0c1209cd6a75836587813198
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overview: module code — osxphotos 0.41.4 documentation</title>
|
||||
<title>Overview: module code — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="../_static/pygments.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>
|
||||
@@ -31,11 +31,7 @@
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>All modules for which code is available</h1>
|
||||
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
|
||||
<ul><li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
|
||||
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.41.4 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.42.16 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.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>
|
||||
@@ -122,6 +122,9 @@
|
||||
<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="n">exported_album</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">skipped_album</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">missing_album</span><span class="o">=</span><span class="kc">None</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">new</span> <span class="o">=</span> <span class="n">new</span> <span class="ow">or</span> <span class="p">[]</span>
|
||||
@@ -144,6 +147,9 @@
|
||||
<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="bp">self</span><span class="o">.</span><span class="n">exported_album</span> <span class="o">=</span> <span class="n">exported_album</span> <span class="ow">or</span> <span class="p">[]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">skipped_album</span> <span class="o">=</span> <span class="n">skipped_album</span> <span class="ow">or</span> <span class="p">[]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">missing_album</span> <span class="o">=</span> <span class="n">missing_album</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="sd">""" return all filenames contained in results """</span>
|
||||
@@ -190,6 +196,10 @@
|
||||
<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="bp">self</span><span class="o">.</span><span class="n">exported_album</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exported_album</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">skipped_album</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">skipped_album</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">missing_album</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">missing_album</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>
|
||||
@@ -214,6 +224,9 @@
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",exported_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exported_album</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",skipped_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">skipped_album</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",missing_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">missing_album</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="s2">")"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
@@ -654,7 +667,11 @@
|
||||
<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_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">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="n">fname</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="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="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">original_filename</span>
|
||||
|
||||
@@ -1687,13 +1704,13 @@
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"QuickTime:ModifyDate"</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="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S"</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">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</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">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</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="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="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</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="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
|
||||
<span class="k">return</span> <span class="n">exif</span>
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.41.4 documentation</title>
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.42.16 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.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>
|
||||
@@ -59,6 +59,10 @@
|
||||
<span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</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">BURST_DEFAULT_PICK</span><span class="p">,</span>
|
||||
<span class="n">BURST_KEY</span><span class="p">,</span>
|
||||
<span class="n">BURST_NOT_SELECTED</span><span class="p">,</span>
|
||||
<span class="n">BURST_SELECTED</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>
|
||||
@@ -486,9 +490,22 @@
|
||||
<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="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">"""If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.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="nb">list</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="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="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">burst_key</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="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">""" list of AlbumInfo objects representing albums the photos is contained in """</span>
|
||||
<span class="sd">""" list of AlbumInfo objects representing albums the photo is contained in """</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">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
@@ -498,6 +515,19 @@
|
||||
<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="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">""" If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.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="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">album_info</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="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">burst_key</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="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">""" ImportInfo object representing import session for the photo or None if no import session """</span>
|
||||
@@ -607,6 +637,23 @@
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">date_added</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" Date photo was added to the database """</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">_date_added</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="n">added_date</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"added_date"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">added_date</span><span class="p">:</span>
|
||||
<span class="n">seconds</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"imageTimeZoneOffsetSeconds"</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span>
|
||||
<span class="n">delta</span> <span class="o">=</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">seconds</span><span class="o">=</span><span class="n">seconds</span><span class="p">)</span>
|
||||
<span class="n">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_date_added</span> <span class="o">=</span> <span class="n">added_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_date_added</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_date_added</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">location</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns (latitude, longitude) as float in degrees or None """</span>
|
||||
@@ -713,6 +760,21 @@
|
||||
<span class="sd">""" Returns True if photo is part of a Burst photo set, otherwise False """</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">"burst"</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">""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """</span>
|
||||
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"burstPickType"</span><span class="p">]</span> <span class="o">&</span> <span class="n">BURST_SELECTED</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">burst_key</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" Returns True if photo is a burst photo and is the key image for the burst set (the image that Photos shows on top of the burst stack), otherwise False """</span>
|
||||
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"burstPickType"</span><span class="p">]</span> <span class="o">&</span> <span class="n">BURST_KEY</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">burst_default_pick</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" Returns True if photo is a burst image and is the photo that Photos selected as the default image for the burst set, otherwise False """</span>
|
||||
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"burstPickType"</span><span class="p">]</span> <span class="o">&</span> <span class="n">BURST_DEFAULT_PICK</span><span class="p">)</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="sd">"""If photo is a burst photo, returns list of PhotoInfo objects</span>
|
||||
@@ -925,6 +987,7 @@
|
||||
<span class="n">filename</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">dirname</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">edited</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""Renders a template string for PhotoInfo instance using PhotoTemplate</span>
|
||||
|
||||
@@ -940,6 +1003,7 @@
|
||||
<span class="sd"> filename: if True, template output will be sanitized to produce valid file name</span>
|
||||
<span class="sd"> dirname: if True, template output will be sanitized to produce valid directory name</span>
|
||||
<span class="sd"> strip: if True, strips leading/trailing white space from resulting template</span>
|
||||
<span class="sd"> edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values</span>
|
||||
@@ -954,6 +1018,7 @@
|
||||
<span class="n">filename</span><span class="o">=</span><span class="n">filename</span><span class="p">,</span>
|
||||
<span class="n">dirname</span><span class="o">=</span><span class="n">dirname</span><span class="p">,</span>
|
||||
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||
<span class="n">edited_version</span><span class="o">=</span><span class="n">edited</span><span class="p">,</span>
|
||||
<span class="p">)</span></div>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.41.4 documentation</title>
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.42.16 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.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>
|
||||
@@ -41,10 +41,14 @@
|
||||
<span class="kn">import</span> <span class="nn">os.path</span>
|
||||
<span class="kn">import</span> <span class="nn">pathlib</span>
|
||||
<span class="kn">import</span> <span class="nn">platform</span>
|
||||
<span class="kn">import</span> <span class="nn">re</span>
|
||||
<span class="kn">import</span> <span class="nn">sys</span>
|
||||
<span class="kn">import</span> <span class="nn">tempfile</span>
|
||||
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||||
<span class="kn">from</span> <span class="nn">pprint</span> <span class="kn">import</span> <span class="n">pformat</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">bitmath</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
|
||||
@@ -62,6 +66,8 @@
|
||||
<span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_TESTED_OS_VERSIONS</span><span class="p">,</span>
|
||||
<span class="n">_UNKNOWN_PERSON</span><span class="p">,</span>
|
||||
<span class="n">BURST_KEY</span><span class="p">,</span>
|
||||
<span class="n">BURST_SELECTED</span><span class="p">,</span>
|
||||
<span class="n">TIME_DELTA</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="kn">from</span> <span class="nn">.._version</span> <span class="kn">import</span> <span class="n">__version__</span>
|
||||
@@ -70,6 +76,7 @@
|
||||
<span class="kn">from</span> <span class="nn">..fileutil</span> <span class="kn">import</span> <span class="n">FileUtil</span>
|
||||
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">PersonInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..photoinfo</span> <span class="kn">import</span> <span class="n">PhotoInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..queryoptions</span> <span class="kn">import</span> <span class="n">QueryOptions</span>
|
||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_check_file_exists</span><span class="p">,</span>
|
||||
<span class="n">_db_is_locked</span><span class="p">,</span>
|
||||
@@ -926,7 +933,8 @@
|
||||
<span class="sd"> RKVersion.subType,</span>
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -957,7 +965,8 @@
|
||||
<span class="sd"> RKVersion.subType,</span>
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1007,6 +1016,7 @@
|
||||
<span class="c1"># 41 RKVersion.inTrashDate</span>
|
||||
<span class="c1"># 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)</span>
|
||||
<span class="c1"># 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)</span>
|
||||
<span class="c1"># 44 RKMaster.importGroupUuid -- to get date added from RKImportGroup</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1095,18 +1105,9 @@
|
||||
<span class="k">if</span> <span class="n">burst_uuid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">[</span><span class="n">burst_uuid</span><span class="p">]</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">[</span><span class="n">burst_uuid</span><span class="p">]</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">uuid</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">24</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">2</span> <span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">24</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span>
|
||||
<span class="s2">"burst_key"</span>
|
||||
<span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># it's a key photo (selected from the burst)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span>
|
||||
<span class="s2">"burst_key"</span>
|
||||
<span class="p">]</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># it's a burst photo but not one that's selected</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># not a burst photo</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"burst"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"burst_key"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># RKVersion.specialType</span>
|
||||
<span class="c1"># 1 == panorama</span>
|
||||
@@ -1209,7 +1210,7 @@
|
||||
|
||||
<span class="c1"># import session not yet handled for Photos 4</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">44</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"fok_import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># get additional details from RKMaster, needed for RAW processing</span>
|
||||
@@ -1399,11 +1400,17 @@
|
||||
|
||||
<span class="c1"># get the place data</span>
|
||||
<span class="n">place_data</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="s2">"SELECT modelID, defaultName, type, area "</span> <span class="s2">"FROM RKPlace "</span>
|
||||
<span class="s2">"SELECT modelID, defaultName, type, area FROM RKPlace"</span>
|
||||
<span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
|
||||
<span class="n">places</span> <span class="o">=</span> <span class="p">{</span><span class="n">p</span><span class="p">[</span><span class="mi">0</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="n">place_data</span><span class="p">}</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_places</span> <span class="o">=</span> <span class="n">places</span>
|
||||
|
||||
<span class="c1"># get import data</span>
|
||||
<span class="n">import_data</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="s2">"SELECT modelID, uuid, name, importDate from RKImportGroup"</span>
|
||||
<span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_import_group</span> <span class="o">=</span> <span class="p">{</span><span class="n">i</span><span class="p">[</span><span class="mi">1</span><span class="p">]:</span> <span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">import_data</span><span class="p">}</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="c1"># get placeId which is then used to lookup defaultName</span>
|
||||
<span class="n">place_ids_query</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
@@ -1437,6 +1444,17 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"placeNames"</span><span class="p">]</span> <span class="o">=</span> <span class="n">place_names</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"reverse_geolocation"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5</span>
|
||||
|
||||
<span class="c1"># add date added</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">import_session</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_import_group</span><span class="p">[</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span>
|
||||
<span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"added_date"</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span>
|
||||
<span class="n">import_session</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="n">TIME_DELTA</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"added_date"</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">(</span><span class="mi">1970</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># build album_titles dictionary</span>
|
||||
<span class="k">for</span> <span class="n">album_id</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">:</span>
|
||||
<span class="n">title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="n">album_id</span><span class="p">][</span><span class="s2">"title"</span><span class="p">]</span>
|
||||
@@ -1861,7 +1879,6 @@
|
||||
|
||||
<span class="c1"># get details about photos</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing photo details."</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Getting information about photos"</span><span class="p">)</span>
|
||||
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"""SELECT </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID, </span>
|
||||
<span class="s2"> ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT, </span>
|
||||
@@ -1903,7 +1920,8 @@
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZADJUSTMENTTIMESTAMP,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZVISIBILITYSTATE,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZTRASHEDDATE,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZSAVEDASSETTYPE</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZSAVEDASSETTYPE,</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZADDEDDATE</span>
|
||||
<span class="s2"> FROM </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2"> </span>
|
||||
<span class="s2"> JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK </span>
|
||||
<span class="s2"> ORDER BY </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID """</span>
|
||||
@@ -1951,6 +1969,7 @@
|
||||
<span class="c1"># 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)</span>
|
||||
<span class="c1"># 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash</span>
|
||||
<span class="c1"># 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported</span>
|
||||
<span class="c1"># 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -2039,18 +2058,9 @@
|
||||
<span class="k">if</span> <span class="n">burst_uuid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">[</span><span class="n">burst_uuid</span><span class="p">]</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">[</span><span class="n">burst_uuid</span><span class="p">]</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">uuid</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">20</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">2</span> <span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">20</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">:</span>
|
||||
<span class="n">info</span><span class="p">[</span>
|
||||
<span class="s2">"burst_key"</span>
|
||||
<span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># it's a key photo (selected from the burst)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">info</span><span class="p">[</span>
|
||||
<span class="s2">"burst_key"</span>
|
||||
<span class="p">]</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># it's a burst photo but not one that's selected</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># not a burst photo</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"burst"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"burst_key"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># Info on sub-type (live photo, panorama, etc)</span>
|
||||
<span class="c1"># ZGENERICASSET.ZKINDSUBTYPE</span>
|
||||
@@ -2139,6 +2149,11 @@
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"saved_asset_type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">40</span><span class="p">]</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"isreference"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">40</span><span class="p">]</span> <span class="o">==</span> <span class="mi">10</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"added_date"</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">41</span><span class="p">]</span> <span class="o">+</span> <span class="n">TIME_DELTA</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">):</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"added_date"</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">(</span><span class="mi">1970</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># initialize import session info which will be filled in later</span>
|
||||
<span class="c1"># not every photo has an import session so initialize all records now</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -2769,8 +2784,6 @@
|
||||
<span class="c1"># an empty album will be in _dbalbum_titles but not _dbalbums_album</span>
|
||||
<span class="k">pass</span>
|
||||
<span class="n">album_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">title_set</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find album '</span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">' in database"</span><span class="p">)</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">album_set</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">uuid</span><span class="p">:</span>
|
||||
@@ -2778,8 +2791,6 @@
|
||||
<span class="k">for</span> <span class="n">u</span> <span class="ow">in</span> <span class="n">uuid</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">u</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="n">uuid_set</span><span class="o">.</span><span class="n">update</span><span class="p">([</span><span class="n">u</span><span class="p">])</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find uuid '</span><span class="si">{</span><span class="n">u</span><span class="si">}</span><span class="s2">' in database"</span><span class="p">)</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">uuid_set</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">keywords</span><span class="p">:</span>
|
||||
@@ -2787,8 +2798,6 @@
|
||||
<span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
|
||||
<span class="n">keyword_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">])</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find keyword '</span><span class="si">{</span><span class="n">keyword</span><span class="si">}</span><span class="s2">' in database"</span><span class="p">)</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_set</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
|
||||
@@ -2801,8 +2810,6 @@
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="c1"># some persons have zero photos so they won't be in _dbfaces_pk</span>
|
||||
<span class="k">pass</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find person '</span><span class="si">{</span><span class="n">person</span><span class="si">}</span><span class="s2">' in database"</span><span class="p">)</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">person_set</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">from_date</span> <span class="ow">or</span> <span class="n">to_date</span><span class="p">:</span> <span class="c1"># sourcery off</span>
|
||||
@@ -2813,14 +2820,10 @@
|
||||
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">"imageDate"</span><span class="p">]</span> <span class="o">>=</span> <span class="n">from_date</span>
|
||||
<span class="p">}</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Found %i items with from_date </span><span class="si">{</span><span class="n">from_date</span><span class="si">}</span><span class="s2">"</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">to_date</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">datetime_has_tz</span><span class="p">(</span><span class="n">to_date</span><span class="p">):</span>
|
||||
<span class="n">to_date</span> <span class="o">=</span> <span class="n">datetime_naive_to_local</span><span class="p">(</span><span class="n">to_date</span><span class="p">)</span>
|
||||
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">"imageDate"</span><span class="p">]</span> <span class="o"><=</span> <span class="n">to_date</span><span class="p">}</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Found %i items with to_date </span><span class="si">{</span><span class="n">to_date</span><span class="si">}</span><span class="s2">"</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">))</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">dsel</span><span class="o">.</span><span class="n">keys</span><span class="p">()))</span>
|
||||
|
||||
<span class="n">photoinfo</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
@@ -2828,7 +2831,10 @@
|
||||
<span class="c1"># get the intersection of each argument/search criteria</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">set</span><span class="o">.</span><span class="n">intersection</span><span class="p">(</span><span class="o">*</span><span class="n">photos_sets</span><span class="p">):</span>
|
||||
<span class="c1"># filter for non-selected burst photos</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">"burst"</span><span class="p">]</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">"burst_key"</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">"burst"</span><span class="p">]</span> <span class="ow">and</span> <span class="ow">not</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">"burstPickType"</span><span class="p">]</span> <span class="o">&</span> <span class="n">BURST_SELECTED</span>
|
||||
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">"burstPickType"</span><span class="p">]</span> <span class="o">&</span> <span class="n">BURST_KEY</span>
|
||||
<span class="p">):</span>
|
||||
<span class="c1"># not a key/selected burst photo, don't include in returned results</span>
|
||||
<span class="k">continue</span>
|
||||
|
||||
@@ -2879,6 +2885,359 @@
|
||||
<span class="k">pass</span>
|
||||
<span class="k">return</span> <span class="n">photos</span></div>
|
||||
|
||||
<div class="viewcode-block" id="PhotosDB.query"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.query">[docs]</a> <span class="k">def</span> <span class="nf">query</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">QueryOptions</span><span class="p">)</span> <span class="o">-></span> <span class="n">List</span><span class="p">[</span><span class="n">PhotoInfo</span><span class="p">]:</span>
|
||||
<span class="sd">"""Run a query against PhotosDB to extract the photos based on user supplied options</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> options: a QueryOptions instance</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">deleted</span> <span class="ow">or</span> <span class="n">options</span><span class="o">.</span><span class="n">deleted_only</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span>
|
||||
<span class="n">uuid</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="n">images</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">photos</span><span class="p">,</span>
|
||||
<span class="n">movies</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">movies</span><span class="p">,</span>
|
||||
<span class="n">from_date</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">from_date</span><span class="p">,</span>
|
||||
<span class="n">to_date</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">to_date</span><span class="p">,</span>
|
||||
<span class="n">intrash</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">options</span><span class="o">.</span><span class="n">deleted_only</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span>
|
||||
<span class="n">uuid</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="n">images</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">photos</span><span class="p">,</span>
|
||||
<span class="n">movies</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">movies</span><span class="p">,</span>
|
||||
<span class="n">from_date</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">from_date</span><span class="p">,</span>
|
||||
<span class="n">to_date</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">to_date</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="n">person</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">person</span><span class="p">)</span>
|
||||
<span class="n">keyword</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">keyword</span><span class="p">)</span>
|
||||
<span class="n">album</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">album</span><span class="p">)</span>
|
||||
<span class="n">folder</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">folder</span><span class="p">)</span>
|
||||
<span class="n">title</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
|
||||
<span class="n">description</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">description</span><span class="p">)</span>
|
||||
<span class="n">place</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">place</span><span class="p">)</span>
|
||||
<span class="n">label</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">label</span><span class="p">)</span>
|
||||
<span class="n">name</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">album</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">_get_photos_by_attribute</span><span class="p">(</span>
|
||||
<span class="n">photos</span><span class="p">,</span> <span class="s2">"albums"</span><span class="p">,</span> <span class="n">album</span><span class="p">,</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">keyword</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">_get_photos_by_attribute</span><span class="p">(</span>
|
||||
<span class="n">photos</span><span class="p">,</span> <span class="s2">"keywords"</span><span class="p">,</span> <span class="n">keyword</span><span class="p">,</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">person</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">_get_photos_by_attribute</span><span class="p">(</span>
|
||||
<span class="n">photos</span><span class="p">,</span> <span class="s2">"persons"</span><span class="p">,</span> <span class="n">person</span><span class="p">,</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">label</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">_get_photos_by_attribute</span><span class="p">(</span>
|
||||
<span class="n">photos</span><span class="p">,</span> <span class="s2">"labels"</span><span class="p">,</span> <span class="n">label</span><span class="p">,</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">folder</span><span class="p">:</span>
|
||||
<span class="c1"># search for photos in an album in folder</span>
|
||||
<span class="c1"># finds photos that have albums whose top level folder matches folder</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">folder</span><span class="p">:</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">album_info</span>
|
||||
<span class="ow">and</span> <span class="n">f</span>
|
||||
<span class="ow">in</span> <span class="p">[</span><span class="n">a</span><span class="o">.</span><span class="n">folder_names</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">album_info</span> <span class="k">if</span> <span class="n">a</span><span class="o">.</span><span class="n">folder_names</span><span class="p">]</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">title</span><span class="p">:</span>
|
||||
<span class="c1"># search title field for text</span>
|
||||
<span class="c1"># if more than one, find photos with all title values in title</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="c1"># case-insensitive</span>
|
||||
<span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">title</span><span class="p">:</span>
|
||||
<span class="n">t</span> <span class="o">=</span> <span class="n">t</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">title</span> <span class="ow">and</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="o">.</span><span class="n">lower</span><span class="p">()]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">title</span><span class="p">:</span>
|
||||
<span class="n">photo_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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">title</span> <span class="ow">and</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="p">])</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_title</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">description</span><span class="p">:</span>
|
||||
<span class="c1"># search description field for text</span>
|
||||
<span class="c1"># if more than one, find photos with all description values in description</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="c1"># case-insensitive</span>
|
||||
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">description</span><span class="p">:</span>
|
||||
<span class="n">d</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">description</span> <span class="ow">and</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">description</span><span class="p">:</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">description</span> <span class="ow">and</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_description</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">place</span><span class="p">:</span>
|
||||
<span class="c1"># search place.names for text matching place</span>
|
||||
<span class="c1"># if more than one place, find photos with all place values in description</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="c1"># case-insensitive</span>
|
||||
<span class="k">for</span> <span class="n">place_name</span> <span class="ow">in</span> <span class="n">place</span><span class="p">:</span>
|
||||
<span class="n">place_name</span> <span class="o">=</span> <span class="n">place_name</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">place</span>
|
||||
<span class="ow">and</span> <span class="nb">any</span><span class="p">(</span>
|
||||
<span class="n">pname</span>
|
||||
<span class="k">for</span> <span class="n">pname</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">place</span><span class="o">.</span><span class="n">names</span>
|
||||
<span class="k">if</span> <span class="nb">any</span><span class="p">(</span>
|
||||
<span class="n">pvalue</span>
|
||||
<span class="k">for</span> <span class="n">pvalue</span> <span class="ow">in</span> <span class="n">pname</span>
|
||||
<span class="k">if</span> <span class="n">place_name</span> <span class="ow">in</span> <span class="n">pvalue</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">]</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">place_name</span> <span class="ow">in</span> <span class="n">place</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">place</span>
|
||||
<span class="ow">and</span> <span class="nb">any</span><span class="p">(</span>
|
||||
<span class="n">pname</span>
|
||||
<span class="k">for</span> <span class="n">pname</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">place</span><span class="o">.</span><span class="n">names</span>
|
||||
<span class="k">if</span> <span class="nb">any</span><span class="p">(</span><span class="n">pvalue</span> <span class="k">for</span> <span class="n">pvalue</span> <span class="ow">in</span> <span class="n">pname</span> <span class="k">if</span> <span class="n">place_name</span> <span class="ow">in</span> <span class="n">pvalue</span><span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_place</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">place</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">edited</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">external_edit</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">external_edit</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">favorite</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">favorite</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_favorite</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">favorite</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">hidden</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hidden</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_hidden</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">hidden</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">missing</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">path</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_missing</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">path</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">shared</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">shared</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_shared</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">shared</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">shared</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">shared</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_shared</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">shared</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">uti</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">uti</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">uti_original</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">burst</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_burst</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">burst</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">live</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">live_photo</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_live</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">live_photo</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">portrait</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">portrait</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_portrait</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">portrait</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">screenshot</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">screenshot</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_screenshot</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">screenshot</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">slow_mo</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">slow_mo</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_slow_mo</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">slow_mo</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">time_lapse</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">time_lapse</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_time_lapse</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">time_lapse</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">hdr</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hdr</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_hdr</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">hdr</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">selfie</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">selfie</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_selfie</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">selfie</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">panorama</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">panorama</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_panorama</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">panorama</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">cloudasset</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">iscloudasset</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_cloudasset</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">iscloudasset</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">incloud</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">incloud</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_incloud</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">incloud</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">has_raw</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">has_raw</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">has_comment</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">comments</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_comment</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">comments</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">has_likes</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">likes</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_likes</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">likes</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">is_reference</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">isreference</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">in_album</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">albums</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_in_album</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">albums</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">from_time</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">>=</span> <span class="n">options</span><span class="o">.</span><span class="n">from_time</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">to_time</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o"><=</span> <span class="n">options</span><span class="o">.</span><span class="n">to_time</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
|
||||
<span class="c1"># add the burst_photos to the export set</span>
|
||||
<span class="n">photos_burst</span> <span class="o">=</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="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">burst</span><span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">burst</span> <span class="ow">in</span> <span class="n">photos_burst</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">missing_bursts</span><span class="p">:</span>
|
||||
<span class="c1"># include burst photos that are missing</span>
|
||||
<span class="n">photos</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">burst</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># don't include missing burst images (these can't be downloaded with AppleScript)</span>
|
||||
<span class="n">photos</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="n">burst</span><span class="o">.</span><span class="n">burst_photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">])</span>
|
||||
|
||||
<span class="c1"># remove duplicates as each burst photo in the set that's selected would</span>
|
||||
<span class="c1"># result in the entire set being added above</span>
|
||||
<span class="c1"># can't use set() because PhotoInfo not hashable</span>
|
||||
<span class="n">seen_uuids</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">in</span> <span class="n">seen_uuids</span><span class="p">:</span>
|
||||
<span class="k">continue</span>
|
||||
<span class="n">seen_uuids</span><span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">seen_uuids</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">name</span><span class="p">:</span>
|
||||
<span class="c1"># search filename fields for text</span>
|
||||
<span class="c1"># if more than one, find photos with all title values in filename</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="c1"># case-insensitive</span>
|
||||
<span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
|
||||
<span class="n">n</span> <span class="o">=</span> <span class="n">n</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span> <span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">min_size</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">bitmath</span><span class="o">.</span><span class="n">Byte</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">original_filesize</span><span class="p">)</span> <span class="o">>=</span> <span class="n">options</span><span class="o">.</span><span class="n">min_size</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">max_size</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">bitmath</span><span class="o">.</span><span class="n">Byte</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">original_filesize</span><span class="p">)</span> <span class="o"><=</span> <span class="n">options</span><span class="o">.</span><span class="n">max_size</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">regex</span><span class="p">:</span>
|
||||
<span class="n">flags</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">IGNORECASE</span> <span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span> <span class="k">else</span> <span class="mi">0</span>
|
||||
<span class="k">for</span> <span class="n">regex</span><span class="p">,</span> <span class="n">template</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">regex</span><span class="p">:</span>
|
||||
<span class="n">regex</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">regex</span><span class="p">,</span> <span class="n">flags</span><span class="p">)</span>
|
||||
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="n">rendered</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">none_str</span><span class="o">=</span><span class="s2">""</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">rendered</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">regex</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">value</span><span class="p">):</span>
|
||||
<span class="n">photo_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">query_eval</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">q</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">query_eval</span><span class="p">:</span>
|
||||
<span class="n">query_string</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"[photo for photo in photos if </span><span class="si">{</span><span class="n">q</span><span class="si">}</span><span class="s2">]"</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="nb">eval</span><span class="p">(</span><span class="n">query_string</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid query_eval CRITERIA: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photos</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"osxphotos.</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="o">.</span><span class="vm">__name__</span><span class="si">}</span><span class="s2">(dbfile='</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">db_path</span><span class="si">}</span><span class="s2">')"</span>
|
||||
|
||||
@@ -2894,6 +3253,34 @@
|
||||
<span class="sd"> Includes recently deleted photos and non-selected burst images</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_photos_by_attribute</span><span class="p">(</span><span class="n">photos</span><span class="p">,</span> <span class="n">attribute</span><span class="p">,</span> <span class="n">values</span><span class="p">,</span> <span class="n">ignore_case</span><span class="p">):</span>
|
||||
<span class="sd">"""Search for photos based on values being in PhotoInfo.attribute</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> photos: a list of PhotoInfo objects</span>
|
||||
<span class="sd"> attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc)</span>
|
||||
<span class="sd"> values: list of values to search in property</span>
|
||||
<span class="sd"> ignore_case: ignore case when searching</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> list of PhotoInfo objects matching search criteria</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">photos_search</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">if</span> <span class="n">ignore_case</span><span class="p">:</span>
|
||||
<span class="c1"># case-insensitive</span>
|
||||
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="n">photos_search</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="n">photos</span>
|
||||
<span class="k">if</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">[</span><span class="n">attr</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">attr</span> <span class="ow">in</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">attribute</span><span class="p">)]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="n">photos_search</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="n">photos</span> <span class="k">if</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">attribute</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="n">photos_search</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
354
docs/_sources/tutorial.md.txt
Normal file
354
docs/_sources/tutorial.md.txt
Normal file
@@ -0,0 +1,354 @@
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
|
||||
# OSXPhotos Tutorial
|
||||
|
||||
## Tutorial
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:END -->
|
||||
|
||||
The design philosophy for osxphotos is "make the easy things easy and make the hard things possible". To "make the hard things possible", osxphotos is very flexible and has many, many configuration options -- the `export` command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the `export` command which exports photos from the library so that's the focus of this tutorial.
|
||||
|
||||
### Export your photos
|
||||
|
||||
`osxphotos export /path/to/export`
|
||||
|
||||
This command exports all your photos to the `/path/to/export` directory.
|
||||
|
||||
**Note**: osxphotos uses the term 'photo' to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple "Live Photo" which is an image and an associated "live preview" video file), a JPEG image with an associated RAW image, etc.
|
||||
|
||||
### Export by date
|
||||
|
||||
While the previous command will export all your photos (and videos--see note above), it probably doesn't do exactly what you want. In the previous example, all the photos will be exported to a single folder: `/path/to/export`. If you have a large library with thousands of images and videos, this likely isn't very useful. You can use the `--export-by-date` option to export photos to a folder structure organized by year, month, day, e.g. `2021/04/21`:
|
||||
|
||||
`osxphotos export /path/to/export --export-by-date`
|
||||
|
||||
With this command, a photo that was created on 31 May 2015 would be exported to: `/path/to/export/2015/05/31`
|
||||
|
||||
### Specify directory structure
|
||||
|
||||
If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START -->template system<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --> that allows you to specify the directory structure using the `--directory` option. For example, this command exported to a directory structure that looks like: `2015/May` (4-digit year / month name):
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{created.month}"`
|
||||
|
||||
The string following `--directory` is an `osxphotos template string`. Template strings are widely used throughout osxphotos and it's worth your time to learn more about them. In a template string, the values between the curly braces, e.g. `{created.year}` are replaced with metadata from the photo being exported. In this case, `{created.year}` is the 4-digit year of the photo's creation date and `{created.month}` is the full month name in the user's locale (e.g. `May`, `mai`, etc.). In the osxphotos template system these are referred to as template fields. The text not included between `{}` pairs is interpreted literally, in this case `/`, is a directory separator.
|
||||
|
||||
osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the `--directory` template, you could thus export photos organized by country name:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country}"`
|
||||
|
||||
Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country,No-Country}"`
|
||||
|
||||
The value after the ',' in the template string is the default value, in this case 'No-Country'. **Note**: If you don't specify a default value and a template field is null, osxphotos will use "_" (underscore character) as the default.
|
||||
|
||||
Some template fields, such as `{keyword}`, may expand to more than one value. For example, if a photo has keywords of "Travel" and "Vacation", `{keyword}` would expand to "Travel", "Vacation". When used with `--directory`, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{keyword}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel/IMG_1234.JPG
|
||||
/path/to/export/Vacation/IMG_1234.JPG
|
||||
|
||||
### Specify exported filename
|
||||
|
||||
By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like `IMG_1234.JPG` or `DSC05678.dng`. osxphotos allows you to specify a custom filename template using the `--filename` option in the same way as `--directory` allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{title}"`
|
||||
|
||||
The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{title,{original_name}}"
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
Use photo's title as the filename <──────┘ ││ │
|
||||
││ │
|
||||
Value after comma will be used <───────┘│ │
|
||||
if title is blank │ │
|
||||
│ │
|
||||
The default value can be <────┘ │
|
||||
another template field │
|
||||
│
|
||||
Use photo's original name if no title <──────┘
|
||||
```
|
||||
|
||||
The osxphotos template system also allows for limited conditional logic of the type "If a condition is true then do one thing, otherwise, do a different thing". For example, you can use the `--filename` option to name files that are marked as "Favorites" in Photos differently than other files. For example, to add a "#" to the name of every photo that's a favorite:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
|
||||
│ │ │││
|
||||
│ │ │││
|
||||
Use photo's original name as filename <──┘ │ │││
|
||||
│ │││
|
||||
'favorite' is True if photo is a Favorite, <───────┘ │││
|
||||
otherwise, False │││
|
||||
│││
|
||||
'?' specifies a conditional <─────────────┘││
|
||||
││
|
||||
Value immediately following ? will be used if <──────┘│
|
||||
preceding template field is True or non-blank │
|
||||
│
|
||||
Value immediately following comma will be used if <──────┘
|
||||
template field is False or blank (null); in this case
|
||||
no value is specified so a blank string "" will be used
|
||||
```
|
||||
|
||||
Like with `--directory`, using a multi-valued template field such as `{keyword}` may result in more than one copy of a photo being exported. For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{keyword}-{original_name}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel-IMG_1234.JPG
|
||||
/path/to/export/Vacation-IMG_1234.JPG
|
||||
|
||||
### Edited photos
|
||||
|
||||
If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append "_edited" to the edited image. For example, if the original image was named `IMG_1234.JPG`, osxphotos will export the original as `IMG_1234.JPG` and the edited version as `IMG_1234_edited.jpeg`. **Note:** Photos changes the extension of edited images to ".jpeg" even if the original was named ".JPG". You can change the suffix appended to edited images using the `--edited-suffix` option:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_EDIT"`
|
||||
|
||||
In this example, the edited image would be named `IMG_1234_EDIT.jpeg`. Like many options in osxphotos, the `--edited-suffix` option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_{modified.year}-{modified.mm}-{modified.dd}"`
|
||||
|
||||
In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: `IMG_1234_2021-04-21.jpeg`.
|
||||
|
||||
You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using `--skip-edited`:
|
||||
|
||||
`osxphotos export /path/to/export --skip-edited`
|
||||
|
||||
You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the `--skip-original-if-edited` option:
|
||||
|
||||
`osxphotos export /path/to/export --skip-original-if-edited`
|
||||
|
||||
As mentioned above, Photos renames JPEG images that have been edited with the ".jpeg" extension. Some applications use ".JPG" and others use ".jpg" or ".JPEG". You can use the `--jpeg-ext` option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. `--jpeg-ext jpg` to use '.jpg' for all JPEGs.
|
||||
|
||||
`osxphotos export /path/to/export --jpeg-ext jpg`
|
||||
|
||||
### Specifying the Photos library
|
||||
|
||||
All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the "Option" key while opening Photos, you can select an alternate Photos library. If you don't specify which library to use, osxphotos will try find the last opened library. Occasionally it can't determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the `--db` option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).
|
||||
|
||||
`osxphotos export /path/to/export --db ~/Pictures/MyAlternateLibrary.photoslibrary`
|
||||
|
||||
### Missing photos
|
||||
|
||||
osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.
|
||||
|
||||
If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the `--download-missing` option. `--download-missing` uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos' AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There's also an experimental `--use-photokit` option that will communicate with Photos using a different "PhotoKit" interface. This option must be used together with `--download-missing`:
|
||||
|
||||
`osxphotos export /path/to/export --download-missing`
|
||||
|
||||
`osxphotos export /path/to/export --download-missing --use-photokit`
|
||||
|
||||
### Exporting to external disks
|
||||
|
||||
If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the `--retry` option so that osxphotos will automatically retry the export. Use `--retry` with a number that specifies the number of times to retry the export:
|
||||
|
||||
`osxphotos export /path/to/export --retry 3`
|
||||
|
||||
In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.
|
||||
|
||||
### Exporting metadata with exported photos
|
||||
|
||||
Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos' native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free [`exiftool`](https://exiftool.org/) app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the `--exiftool` option to write metadata to exported photos:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool`
|
||||
|
||||
This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with `--exiftool` to modify the metadata that is written by `exiftool`. For example, you can use the `--keyword-template` option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
|
||||
│ │
|
||||
│ │
|
||||
folder_album results in the folder(s) <──┘ │
|
||||
and album a photo is contained in │
|
||||
│
|
||||
The value in () is used as the path separator <───────┘
|
||||
for joining the folders and albums. For example,
|
||||
if photo is in Folder1/Folder2/Album, (>) produces
|
||||
"Folder1>Folder2>Album" which some programs, such as
|
||||
Lightroom Classic, treat as hierarchal keywords
|
||||
```
|
||||
|
||||
The above command will write all the regular metadata that `--exiftool` normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form "Folder1>Folder2>Album". If you did not include the `(>)` in the template string (e.g. `{folder_album}`), folder_album would render in form "Folder1/Folder2/Album".
|
||||
|
||||
A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the `{label}`. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image's metadata:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label}"`
|
||||
|
||||
**Note**: When evaluating templates for `--directory` and `--filename`, osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as `--keyword-template`, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"`
|
||||
|
||||
### Sidecar files
|
||||
|
||||
Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the `--sidecar` option. For example, to output metadata to XMP sidecars:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP`
|
||||
|
||||
Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`.
|
||||
|
||||
Sidecar files are named "photoname.ext.sidecar_ext". For example, if the photo is named `IMG_1234.JPG` and the sidecar format is XMP, the sidecar would be named `IMG_1234.JPG.XMP`. Some applications expect the sidecar in this case to be named `IMG_1234.XMP`. You can use the `-sidecar-drop-ext` option to force osxphotos to name the sidecar files in this manner:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP -sidecar-drop-ext`
|
||||
|
||||
### Updating a previous export
|
||||
|
||||
If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the `--update` option. When `osxphotos export` is run, it creates a database file named `.osxphotos_export.db` in the export folder. (**Note** Because the filename starts with a ".", you won't see it in Finder which treats "dot-files" like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the `--update` option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:
|
||||
|
||||
`osxphotos export /path/to/export --update`
|
||||
|
||||
will read the export database located in `/path/to/export/.osxphotos_export.db` and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the `--update` option even if it's never been run before. If the database isn't found, osxphotos will create it. If you run `osxphotos export` without `--update` in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use `--update`.
|
||||
|
||||
If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of `--update`, you can use the `--only-new` with `--update` to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without `--only-new`, osxphotos would see that previously exported files are missing and re-export them.
|
||||
|
||||
`osxphotos export /path/to/export --update --only-new`
|
||||
|
||||
If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with `--update`, you should use the `--ignore-signature` option. `--ignore-signature` instructs osxphotos to ignore the file's signature (for example, size and date modified) when deciding which files should be updated with `--update`. If you edit a file in the export directory and then run `--update` without `--ignore-signature`, osxphotos will see that the file is different than the one in the Photos library and re-export it.
|
||||
|
||||
`osxphotos export /path/to/export --update --ignore-signature`
|
||||
|
||||
### Dry Run
|
||||
|
||||
You can use the `--dry-run` option to have osxphotos "dry run" or test an export without actually exporting any files. When combined with the `--verbose` option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your `--directory` and `--filename` templates are correct, `--dry-run --verbose` will print out the name of each file being exported.
|
||||
|
||||
`osxphotos export /path/to/export --dry-run --verbose`
|
||||
|
||||
### Creating a report of all exported files
|
||||
|
||||
You can use the `--report` option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the `--report` option:
|
||||
|
||||
`osxphotos export /path/to/export --report export.csv`
|
||||
|
||||
### Exporting only certain photos
|
||||
|
||||
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
|
||||
|
||||
For example, to export only photos with keyword `Travel`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel"`
|
||||
|
||||
Like many options in osxphotos, `--keyword` (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword `Travel` *or* keyword `Vacation`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel" --keyword "Vacation"`
|
||||
|
||||
To export only photos contained in the album "Summer Vacation":
|
||||
|
||||
`osxphotos export /path/to/export --album "Summer Vacation"`
|
||||
|
||||
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
|
||||
|
||||
`osxphotos export /path/to/export --portrait`
|
||||
|
||||
You can also export photos in a certain date range:
|
||||
|
||||
`osxphotos export /path/to/export --from-date "2020-01-01" --to-date "2020-02-28"`
|
||||
|
||||
### Converting images to JPEG on export
|
||||
|
||||
Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the `--convert-to-jpeg` option. You can specify the JPEG quality (0: worst, 1.0: best) using `--jpeg-quality`. For example:
|
||||
|
||||
`osxphotos export /path/to/export --convert-to-jpeg --jpeg-quality 0.9`
|
||||
|
||||
### Finder attributes
|
||||
|
||||
In addition to using `exiftool` to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.
|
||||
|
||||
osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the `tag:tagname` syntax:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords`
|
||||
|
||||
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords --keyword-template "{label}"`
|
||||
|
||||
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
|
||||
|
||||
For example, to set Finder comment to the photo's title and description:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{newline}{descr}"`
|
||||
|
||||
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
|
||||
|
||||
Explanation of the template string:
|
||||
|
||||
```txt
|
||||
{title}{title?{descr?{newline},},}{descr}
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──> insert title │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└───> is there a title?
|
||||
│ │ │ │ │
|
||||
└───> if so, is there a description?
|
||||
│ │ │ │
|
||||
└───> if so, insert new line
|
||||
│ │ │
|
||||
└───> if descr is blank, insert nothing
|
||||
│ │
|
||||
└───> if title is blank, insert nothing
|
||||
│
|
||||
└───> finally, insert description
|
||||
```
|
||||
|
||||
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
|
||||
|
||||
The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.
|
||||
|
||||
See Extended Attributes section in the help for `osxphotos export` for additional information about this feature.
|
||||
|
||||
### Saving and loading options
|
||||
|
||||
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
|
||||
|
||||
To save the configuration:
|
||||
|
||||
`osxphotos export /path/to/export <all your options here> --update --save-config osxphotos.toml`
|
||||
|
||||
Then the next to you run osxphotos, you can simply do this:
|
||||
|
||||
`osxphotos export /path/to/export --load-config osxphotos.toml`
|
||||
|
||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||
|
||||
### An example from an actual osxphotos user
|
||||
|
||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||
|
||||
I usually import my iPhone’s photo roll on a more or less regular basis, and it
|
||||
includes photos and videos. As a result, the size ot my Photos library may rise
|
||||
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
|
||||
quite good keyword management system.
|
||||
|
||||
After a while, I want to take most of the videos out of the library and move them
|
||||
to a separate "videos" folder on a different folder / volume. As I might want to
|
||||
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
|
||||
its internal library tagging system, I will use osxphotos to do just this.
|
||||
|
||||
Picking the videos can be left to Photos, using a smart folder for instance. Then
|
||||
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
|
||||
to spot all videos created on my iPhone using the Quik application (now part of
|
||||
GoPro).
|
||||
|
||||
I want to retrieve my keywords only and make sure they populate the Finder tags, as
|
||||
well as export all the persons identified in the videos by Photos. I also want to
|
||||
merge any keywords or persons already in the video metadata with the exported
|
||||
metadata.
|
||||
|
||||
Keeping Photo’s edited titles and descriptions and putting both in the Finder
|
||||
comments field in a readable manner is also enabled.
|
||||
|
||||
And I want to keep the file’s creation date (using `--touch-file`).
|
||||
|
||||
Finally, use `--strip` to remove any leading or trailing whitespace from processed
|
||||
template fields.
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.41.4',
|
||||
VERSION: '0.42.17',
|
||||
LANGUAGE: 'None',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos command line interface (CLI) — osxphotos 0.41.4 documentation</title>
|
||||
<title>osxphotos command line interface (CLI) — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.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>
|
||||
@@ -183,6 +183,12 @@ to modify this behavior.</p>
|
||||
<dd><p>Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-name">
|
||||
<code class="sig-name descname"><span class="pre">--name</span></code><code class="sig-prename descclassname"> <span class="pre"><FILENAME></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-name" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with filename matching FILENAME. If more than one –name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-uuid">
|
||||
<code class="sig-name descname"><span class="pre">--uuid</span></code><code class="sig-prename descclassname"> <span class="pre"><UUID></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-uuid" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -489,6 +495,30 @@ to modify this behavior.</p>
|
||||
<dd><p>Search for photos that are not in any albums.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-min-size">
|
||||
<code class="sig-name descname"><span class="pre">--min-size</span></code><code class="sig-prename descclassname"> <span class="pre"><SIZE></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-min-size" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with size >= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-max-size">
|
||||
<code class="sig-name descname"><span class="pre">--max-size</span></code><code class="sig-prename descclassname"> <span class="pre"><SIZE></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-max-size" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with size <= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-regex">
|
||||
<code class="sig-name descname"><span class="pre">--regex</span></code><code class="sig-prename descclassname"> <span class="pre"><REGEX</span> <span class="pre">TEMPLATE></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-regex" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos where TEMPLATE matches regular expression REGEX. For example, to find photos in an album that begins with ‘Beach’: ‘–regex “^Beach” “{album}”’. You may specify more than one regular expression match by repeating ‘–regex’ with different arguments.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-query-eval">
|
||||
<code class="sig-name descname"><span class="pre">--query-eval</span></code><code class="sig-prename descclassname"> <span class="pre"><CRITERIA></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-query-eval" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: <cite>photos = [photo for photo in photos if CRITERIA]</cite> where photo represents a PhotoInfo object. For example: <cite>–query-eval photo.favorite</cite> returns all photos that have been favorited and is equivalent to –favorite. You may specify more than one CRITERIA by using –query-eval multiple times. CRITERIA must be a valid python expression. See <a class="reference external" href="https://rhettbull.github.io/osxphotos/">https://rhettbull.github.io/osxphotos/</a> for additional documentation on the PhotoInfo class.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-missing">
|
||||
<code class="sig-name descname"><span class="pre">--missing</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-missing" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -549,6 +579,12 @@ to modify this behavior.</p>
|
||||
<dd><p>Overwrite existing files. Default behavior is to add (1), (2), etc to filename if file already exists. Use this with caution as it may create name collisions on export. (e.g. if two files happen to have the same name)</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-retry">
|
||||
<code class="sig-name descname"><span class="pre">--retry</span></code><code class="sig-prename descclassname"> <span class="pre"><RETRY></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-retry" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Automatically retry export up to RETRY times if an error occurs during export. This may be useful with network drives that experience intermittent errors.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-export-by-date">
|
||||
<code class="sig-name descname"><span class="pre">--export-by-date</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-export-by-date" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -777,6 +813,24 @@ to modify this behavior.</p>
|
||||
<dd><p>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. WARNING: –cleanup will delete <em>any</em> 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.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-add-exported-to-album">
|
||||
<code class="sig-name descname"><span class="pre">--add-exported-to-album</span></code><code class="sig-prename descclassname"> <span class="pre"><ALBUM></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-add-exported-to-album" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Add all exported photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All exported photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-add-skipped-to-album">
|
||||
<code class="sig-name descname"><span class="pre">--add-skipped-to-album</span></code><code class="sig-prename descclassname"> <span class="pre"><ALBUM></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-add-skipped-to-album" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Add all skipped photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All skipped photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-add-missing-to-album">
|
||||
<code class="sig-name descname"><span class="pre">--add-missing-to-album</span></code><code class="sig-prename descclassname"> <span class="pre"><ALBUM></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-add-missing-to-album" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Add all missing photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All missing photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-export-exportdb">
|
||||
<code class="sig-name descname"><span class="pre">--exportdb</span></code><code class="sig-prename descclassname"> <span class="pre"><EXPORTDB_FILE></span></code><a class="headerlink" href="#cmdoption-osxphotos-export-exportdb" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -1017,6 +1071,12 @@ if more than one option is provided, they are treated as “AND”
|
||||
<dd><p>Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-name">
|
||||
<code class="sig-name descname"><span class="pre">--name</span></code><code class="sig-prename descclassname"> <span class="pre"><FILENAME></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-name" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with filename matching FILENAME. If more than one –name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-uuid">
|
||||
<code class="sig-name descname"><span class="pre">--uuid</span></code><code class="sig-prename descclassname"> <span class="pre"><UUID></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-uuid" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -1323,6 +1383,30 @@ if more than one option is provided, they are treated as “AND”
|
||||
<dd><p>Search for photos that are not in any albums.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-min-size">
|
||||
<code class="sig-name descname"><span class="pre">--min-size</span></code><code class="sig-prename descclassname"> <span class="pre"><SIZE></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-min-size" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with size >= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-max-size">
|
||||
<code class="sig-name descname"><span class="pre">--max-size</span></code><code class="sig-prename descclassname"> <span class="pre"><SIZE></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-max-size" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos with size <= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-regex">
|
||||
<code class="sig-name descname"><span class="pre">--regex</span></code><code class="sig-prename descclassname"> <span class="pre"><REGEX</span> <span class="pre">TEMPLATE></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-regex" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Search for photos where TEMPLATE matches regular expression REGEX. For example, to find photos in an album that begins with ‘Beach’: ‘–regex “^Beach” “{album}”’. You may specify more than one regular expression match by repeating ‘–regex’ with different arguments.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-query-eval">
|
||||
<code class="sig-name descname"><span class="pre">--query-eval</span></code><code class="sig-prename descclassname"> <span class="pre"><CRITERIA></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-query-eval" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: <cite>photos = [photo for photo in photos if CRITERIA]</cite> where photo represents a PhotoInfo object. For example: <cite>–query-eval photo.favorite</cite> returns all photos that have been favorited and is equivalent to –favorite. You may specify more than one CRITERIA by using –query-eval multiple times. CRITERIA must be a valid python expression. See <a class="reference external" href="https://rhettbull.github.io/osxphotos/">https://rhettbull.github.io/osxphotos/</a> for additional documentation on the PhotoInfo class.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-deleted">
|
||||
<code class="sig-name descname"><span class="pre">--deleted</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-deleted" title="Permalink to this definition">¶</a></dt>
|
||||
@@ -1371,6 +1455,12 @@ if more than one option is provided, they are treated as “AND”
|
||||
<dd><p>Search for photos that are not in iCloud (have not been synched)</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-add-to-album">
|
||||
<code class="sig-name descname"><span class="pre">--add-to-album</span></code><code class="sig-prename descclassname"> <span class="pre"><ALBUM></span></code><a class="headerlink" href="#cmdoption-osxphotos-query-add-to-album" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Add all photos from query to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All photos in the query results will be added to this album. This only works if the Photos library being queried is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large query sets.</p>
|
||||
</dd></dl>
|
||||
|
||||
<p class="rubric">Arguments</p>
|
||||
<dl class="std option">
|
||||
<dt id="cmdoption-osxphotos-query-arg-PHOTOS_LIBRARY">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Index — osxphotos 0.41.4 documentation</title>
|
||||
<title>Index — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.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>
|
||||
@@ -51,6 +51,7 @@
|
||||
| <a href="#N"><strong>N</strong></a>
|
||||
| <a href="#O"><strong>O</strong></a>
|
||||
| <a href="#P"><strong>P</strong></a>
|
||||
| <a href="#Q"><strong>Q</strong></a>
|
||||
| <a href="#R"><strong>R</strong></a>
|
||||
| <a href="#S"><strong>S</strong></a>
|
||||
| <a href="#T"><strong>T</strong></a>
|
||||
@@ -64,6 +65,34 @@
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li>
|
||||
--add-exported-to-album <ALBUM>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-exported-to-album">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--add-missing-to-album <ALBUM>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-missing-to-album">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--add-skipped-to-album <ALBUM>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-skipped-to-album">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--add-to-album <ALBUM>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-add-to-album">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--album <ALBUM>
|
||||
|
||||
<ul>
|
||||
@@ -509,6 +538,24 @@
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--max-size <SIZE>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-max-size">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-max-size">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--min-size <SIZE>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-min-size">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-min-size">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -518,6 +565,15 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-missing">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-missing">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--name <FILENAME>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-name">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-name">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -529,6 +585,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-comment">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li>
|
||||
--no-description
|
||||
|
||||
@@ -538,8 +596,6 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li>
|
||||
--no-likes
|
||||
|
||||
@@ -785,6 +841,24 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--query-eval <CRITERIA>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--regex <REGEX TEMPLATE>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-regex">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-regex">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -799,6 +873,13 @@
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--retry <RETRY>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-retry">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -1089,13 +1170,23 @@
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
|
||||
</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>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_default_pick">burst_default_pick() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_key">burst_key() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<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>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
@@ -1126,6 +1217,8 @@
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.date">date() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.date_added">date_added() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.date_modified">date_modified() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
@@ -1427,6 +1520,12 @@
|
||||
osxphotos-export command line option
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-exported-to-album">--add-exported-to-album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-missing-to-album">--add-missing-to-album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-add-skipped-to-album">--add-skipped-to-album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-album">--album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-album-keyword">--album-keyword</a>
|
||||
@@ -1524,8 +1623,14 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-live">--live</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config <config file path></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-max-size">--max-size <SIZE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-min-size">--min-size <SIZE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-missing">--missing</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-name">--name <FILENAME></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-no-comment">--no-comment</a>
|
||||
</li>
|
||||
@@ -1582,10 +1687,16 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-place">--place <PLACE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">--query-eval <CRITERIA></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-regex">--regex <REGEX TEMPLATE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report <path to export report></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-retry">--retry <RETRY></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config <config file path></a>
|
||||
</li>
|
||||
@@ -1723,6 +1834,8 @@
|
||||
osxphotos-query command line option
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-add-to-album">--add-to-album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-album">--album <ALBUM></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-burst">--burst</a>
|
||||
@@ -1774,8 +1887,14 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-label">--label <LABEL></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-live">--live</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-max-size">--max-size <SIZE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-min-size">--min-size <SIZE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-missing">--missing</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-name">--name <FILENAME></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-comment">--no-comment</a>
|
||||
</li>
|
||||
@@ -1830,6 +1949,10 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-place">--place <PLACE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">--portrait</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">--query-eval <CRITERIA></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-regex">--regex <REGEX TEMPLATE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a>
|
||||
</li>
|
||||
@@ -1958,6 +2081,14 @@
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
|
||||
<h2 id="Q">Q</h2>
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#osxphotos.PhotosDB.query">query() (osxphotos.PhotosDB method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
|
||||
<h2 id="R">R</h2>
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.41.4 documentation</title>
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.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>
|
||||
@@ -150,6 +150,10 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
<h4>export default library using ‘country name/year’ as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">"Big</span> <span class="pre">Videos"</span></code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="example-uses-of-the-package">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos — osxphotos 0.41.4 documentation</title>
|
||||
<title>osxphotos — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.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>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.41.4 documentation</title>
|
||||
<title>Search — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
367
docs/tutorial.html
Normal file
367
docs/tutorial.html
Normal file
@@ -0,0 +1,367 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Export your photos — osxphotos 0.42.17 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.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 src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
|
||||
# OSXPhotos Tutorial
|
||||
|
||||
## Tutorial
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:END --><p>The design philosophy for osxphotos is “make the easy things easy and make the hard things possible”. To “make the hard things possible”, osxphotos is very flexible and has many, many configuration options – the <code class="docutils literal notranslate"><span class="pre">export</span></code> command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the <code class="docutils literal notranslate"><span class="pre">export</span></code> command which exports photos from the library so that’s the focus of this tutorial.</p>
|
||||
<div class="section" id="export-your-photos">
|
||||
<h1>Export your photos<a class="headerlink" href="#export-your-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span></code></p>
|
||||
<p>This command exports all your photos to the <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code> directory.</p>
|
||||
<p><strong>Note</strong>: osxphotos uses the term ‘photo’ to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple “Live Photo” which is an image and an associated “live preview” video file), a JPEG image with an associated RAW image, etc.</p>
|
||||
</div>
|
||||
<div class="section" id="export-by-date">
|
||||
<h1>Export by date<a class="headerlink" href="#export-by-date" title="Permalink to this headline">¶</a></h1>
|
||||
<p>While the previous command will export all your photos (and videos–see note above), it probably doesn’t do exactly what you want. In the previous example, all the photos will be exported to a single folder: <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code>. If you have a large library with thousands of images and videos, this likely isn’t very useful. You can use the <code class="docutils literal notranslate"><span class="pre">--export-by-date</span></code> option to export photos to a folder structure organized by year, month, day, e.g. <code class="docutils literal notranslate"><span class="pre">2021/04/21</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--export-by-date</span></code></p>
|
||||
<p>With this command, a photo that was created on 31 May 2015 would be exported to: <code class="docutils literal notranslate"><span class="pre">/path/to/export/2015/05/31</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="specify-directory-structure">
|
||||
<h1>Specify directory structure<a class="headerlink" href="#specify-directory-structure" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START --></span>template system<span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --></span> that allows you to specify the directory structure using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option. For example, this command exported to a directory structure that looks like: <code class="docutils literal notranslate"><span class="pre">2015/May</span></code> (4-digit year / month name):</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{created.month}"</span></code></p>
|
||||
<p>The string following <code class="docutils literal notranslate"><span class="pre">--directory</span></code> is an <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">template</span> <span class="pre">string</span></code>. Template strings are widely used throughout osxphotos and it’s worth your time to learn more about them. In a template string, the values between the curly braces, e.g. <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> are replaced with metadata from the photo being exported. In this case, <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> is the 4-digit year of the photo’s creation date and <code class="docutils literal notranslate"><span class="pre">{created.month}</span></code> is the full month name in the user’s locale (e.g. <code class="docutils literal notranslate"><span class="pre">May</span></code>, <code class="docutils literal notranslate"><span class="pre">mai</span></code>, etc.). In the osxphotos template system these are referred to as template fields. The text not included between <code class="docutils literal notranslate"><span class="pre">{}</span></code> pairs is interpreted literally, in this case <code class="docutils literal notranslate"><span class="pre">/</span></code>, is a directory separator.</p>
|
||||
<p>osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> template, you could thus export photos organized by country name:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{place.name.country}"</span></code></p>
|
||||
<p>Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{place.name.country,No-Country}"</span></code></p>
|
||||
<p>The value after the ‘,’ in the template string is the default value, in this case ‘No-Country’. <strong>Note</strong>: If you don’t specify a default value and a template field is null, osxphotos will use “_” (underscore character) as the default.</p>
|
||||
<p>Some template fields, such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code>, may expand to more than one value. For example, if a photo has keywords of “Travel” and “Vacation”, <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> would expand to “Travel”, “Vacation”. When used with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{keyword}"</span></code></p>
|
||||
<p>the exported files would be:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="specify-exported-filename">
|
||||
<h1>Specify exported filename<a class="headerlink" href="#specify-exported-filename" title="Permalink to this headline">¶</a></h1>
|
||||
<p>By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> or <code class="docutils literal notranslate"><span class="pre">DSC05678.dng</span></code>. osxphotos allows you to specify a custom filename template using the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option in the same way as <code class="docutils literal notranslate"><span class="pre">--directory</span></code> allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">"{title}"</span></code></p>
|
||||
<p>The above command will export photos using the title. Note that you don’t need to specify the extension as part of the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename "{title,{original_name}}"
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
Use photo's title as the filename <──────┘ ││ │
|
||||
││ │
|
||||
Value after comma will be used <───────┘│ │
|
||||
if title is blank │ │
|
||||
│ │
|
||||
The default value can be <────┘ │
|
||||
another template field │
|
||||
│
|
||||
Use photo's original name if no title <──────┘
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The osxphotos template system also allows for limited conditional logic of the type “If a condition is true then do one thing, otherwise, do a different thing”. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option to name files that are marked as “Favorites” in Photos differently than other files. For example, to add a “#” to the name of every photo that’s a favorite:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
|
||||
│ │ │││
|
||||
│ │ │││
|
||||
Use photo's original name as filename <──┘ │ │││
|
||||
│ │││
|
||||
'favorite' is True if photo is a Favorite, <───────┘ │││
|
||||
otherwise, False │││
|
||||
│││
|
||||
'?' specifies a conditional <─────────────┘││
|
||||
││
|
||||
Value immediately following ? will be used if <──────┘│
|
||||
preceding template field is True or non-blank │
|
||||
│
|
||||
Value immediately following comma will be used if <──────┘
|
||||
template field is False or blank (null); in this case
|
||||
no value is specified so a blank string "" will be used
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Like with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, using a multi-valued template field such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> may result in more than one copy of a photo being exported. For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">"{keyword}-{original_name}"</span></code></p>
|
||||
<p>the exported files would be:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="edited-photos">
|
||||
<h1>Edited photos<a class="headerlink" href="#edited-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append “_edited” to the edited image. For example, if the original image was named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code>, osxphotos will export the original as <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the edited version as <code class="docutils literal notranslate"><span class="pre">IMG_1234_edited.jpeg</span></code>. <strong>Note:</strong> Photos changes the extension of edited images to “.jpeg” even if the original was named “.JPG”. You can change the suffix appended to edited images using the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">"_EDIT"</span></code></p>
|
||||
<p>In this example, the edited image would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234_EDIT.jpeg</span></code>. Like many options in osxphotos, the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">"_{modified.year}-{modified.mm}-{modified.dd}"</span></code></p>
|
||||
<p>In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: <code class="docutils literal notranslate"><span class="pre">IMG_1234_2021-04-21.jpeg</span></code>.</p>
|
||||
<p>You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using <code class="docutils literal notranslate"><span class="pre">--skip-edited</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-edited</span></code></p>
|
||||
<p>You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the <code class="docutils literal notranslate"><span class="pre">--skip-original-if-edited</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-original-if-edited</span></code></p>
|
||||
<p>As mentioned above, Photos renames JPEG images that have been edited with the “.jpeg” extension. Some applications use “.JPG” and others use “.jpg” or “.JPEG”. You can use the <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span></code> option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code> to use ‘.jpg’ for all JPEGs.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="specifying-the-photos-library">
|
||||
<h1>Specifying the Photos library<a class="headerlink" href="#specifying-the-photos-library" title="Permalink to this headline">¶</a></h1>
|
||||
<p>All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the “Option” key while opening Photos, you can select an alternate Photos library. If you don’t specify which library to use, osxphotos will try find the last opened library. Occasionally it can’t determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the <code class="docutils literal notranslate"><span class="pre">--db</span></code> option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--db</span> <span class="pre">~/Pictures/MyAlternateLibrary.photoslibrary</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="missing-photos">
|
||||
<h1>Missing photos<a class="headerlink" href="#missing-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn’t yet synched the cloud library to the local Mac or you have Photos configured to “Optimize Mac Storage” in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.</p>
|
||||
<p>If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> option. <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos’ AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There’s also an experimental <code class="docutils literal notranslate"><span class="pre">--use-photokit</span></code> option that will communicate with Photos using a different “PhotoKit” interface. This option must be used together with <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span></code></p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span> <span class="pre">--use-photokit</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="exporting-to-external-disks">
|
||||
<h1>Exporting to external disks<a class="headerlink" href="#exporting-to-external-disks" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the <code class="docutils literal notranslate"><span class="pre">--retry</span></code> option so that osxphotos will automatically retry the export. Use <code class="docutils literal notranslate"><span class="pre">--retry</span></code> with a number that specifies the number of times to retry the export:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--retry</span> <span class="pre">3</span></code></p>
|
||||
<p>In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.</p>
|
||||
</div>
|
||||
<div class="section" id="exporting-metadata-with-exported-photos">
|
||||
<h1>Exporting metadata with exported photos<a class="headerlink" href="#exporting-metadata-with-exported-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos’ native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free <cite>``exiftool`</cite> <<a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>>`_ app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> option to write metadata to exported photos:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span></code></p>
|
||||
<p>This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify the metadata that is written by <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
|
||||
│ │
|
||||
│ │
|
||||
folder_album results in the folder(s) <──┘ │
|
||||
and album a photo is contained in │
|
||||
│
|
||||
The value in () is used as the path separator <───────┘
|
||||
for joining the folders and albums. For example,
|
||||
if photo is in Folder1/Folder2/Album, (>) produces
|
||||
"Folder1>Folder2>Album" which some programs, such as
|
||||
Lightroom Classic, treat as hierarchal keywords
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The above command will write all the regular metadata that <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form “Folder1>Folder2>Album”. If you did not include the <code class="docutils literal notranslate"><span class="pre">(>)</span></code> in the template string (e.g. <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>), folder_album would render in form “Folder1/Folder2/Album”.</p>
|
||||
<p>A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the <code class="docutils literal notranslate"><span class="pre">{label}</span></code>. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image’s metadata:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span></code></p>
|
||||
<p><strong>Note</strong>: When evaluating templates for <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code>, osxphotos inserts the automatic default value “_” for any template field which is null (empty or blank). This is to ensure that there’s never a null directory or filename created. For metadata templates such as <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code>, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add “nolabel” as a keyword for any photo that doesn’t have labels:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label,nolabel}"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="sidecar-files">
|
||||
<h1>Sidecar files<a class="headerlink" href="#sidecar-files" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> option. For example, to output metadata to XMP sidecars:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span></code></p>
|
||||
<p>Unlike <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code>, you do not need to install exiftool to use the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> feature. Many of the same configuration options that apply to <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify metadata, for example, <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> can also be used with <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code>.</p>
|
||||
<p>Sidecar files are named “photoname.ext.sidecar_ext”. For example, if the photo is named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the sidecar format is XMP, the sidecar would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG.XMP</span></code>. Some applications expect the sidecar in this case to be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.XMP</span></code>. You can use the <code class="docutils literal notranslate"><span class="pre">-sidecar-drop-ext</span></code> option to force osxphotos to name the sidecar files in this manner:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span> <span class="pre">-sidecar-drop-ext</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="updating-a-previous-export">
|
||||
<h1>Updating a previous export<a class="headerlink" href="#updating-a-previous-export" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option. When <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> is run, it creates a database file named <code class="docutils literal notranslate"><span class="pre">.osxphotos_export.db</span></code> in the export folder. (<strong>Note</strong> Because the filename starts with a “.”, you won’t see it in Finder which treats “dot-files” like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span></code></p>
|
||||
<p>will read the export database located in <code class="docutils literal notranslate"><span class="pre">/path/to/export/.osxphotos_export.db</span></code> and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option even if it’s never been run before. If the database isn’t found, osxphotos will create it. If you run <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> without <code class="docutils literal notranslate"><span class="pre">--update</span></code> in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use <code class="docutils literal notranslate"><span class="pre">--update</span></code>.</p>
|
||||
<p>If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you can use the <code class="docutils literal notranslate"><span class="pre">--only-new</span></code> with <code class="docutils literal notranslate"><span class="pre">--update</span></code> to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without <code class="docutils literal notranslate"><span class="pre">--only-new</span></code>, osxphotos would see that previously exported files are missing and re-export them.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--only-new</span></code></p>
|
||||
<p>If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you should use the <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> option. <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> instructs osxphotos to ignore the file’s signature (for example, size and date modified) when deciding which files should be updated with <code class="docutils literal notranslate"><span class="pre">--update</span></code>. If you edit a file in the export directory and then run <code class="docutils literal notranslate"><span class="pre">--update</span></code> without <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code>, osxphotos will see that the file is different than the one in the Photos library and re-export it.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--ignore-signature</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="dry-run">
|
||||
<h1>Dry Run<a class="headerlink" href="#dry-run" title="Permalink to this headline">¶</a></h1>
|
||||
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--dry-run</span></code> option to have osxphotos “dry run” or test an export without actually exporting any files. When combined with the <code class="docutils literal notranslate"><span class="pre">--verbose</span></code> option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code> templates are correct, <code class="docutils literal notranslate"><span class="pre">--dry-run</span> <span class="pre">--verbose</span></code> will print out the name of each file being exported.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--dry-run</span> <span class="pre">--verbose</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="creating-a-report-of-all-exported-files">
|
||||
<h1>Creating a report of all exported files<a class="headerlink" href="#creating-a-report-of-all-exported-files" title="Permalink to this headline">¶</a></h1>
|
||||
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--report</span> <span class="pre">export.csv</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="exporting-only-certain-photos">
|
||||
<h1>Exporting only certain photos<a class="headerlink" href="#exporting-only-certain-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of “query options” that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them–read the help text (<code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">export</span></code>) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.</p>
|
||||
<p>For example, to export only photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">"Travel"</span></code></p>
|
||||
<p>Like many options in osxphotos, <code class="docutils literal notranslate"><span class="pre">--keyword</span></code> (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code> <em>or</em> keyword <code class="docutils literal notranslate"><span class="pre">Vacation</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">"Travel"</span> <span class="pre">--keyword</span> <span class="pre">"Vacation"</span></code></p>
|
||||
<p>To export only photos contained in the album “Summer Vacation”:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--album</span> <span class="pre">"Summer</span> <span class="pre">Vacation"</span></code></p>
|
||||
<p>There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone “Portrait Mode”:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--portrait</span></code></p>
|
||||
<p>You can also export photos in a certain date range:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--from-date</span> <span class="pre">"2020-01-01"</span> <span class="pre">--to-date</span> <span class="pre">"2020-02-28"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="converting-images-to-jpeg-on-export">
|
||||
<h1>Converting images to JPEG on export<a class="headerlink" href="#converting-images-to-jpeg-on-export" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the <code class="docutils literal notranslate"><span class="pre">--convert-to-jpeg</span></code> option. You can specify the JPEG quality (0: worst, 1.0: best) using <code class="docutils literal notranslate"><span class="pre">--jpeg-quality</span></code>. For example:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--convert-to-jpeg</span> <span class="pre">--jpeg-quality</span> <span class="pre">0.9</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="finder-attributes">
|
||||
<h1>Finder attributes<a class="headerlink" href="#finder-attributes" title="Permalink to this headline">¶</a></h1>
|
||||
<p>In addition to using <code class="docutils literal notranslate"><span class="pre">exiftool</span></code> to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.</p>
|
||||
<p>osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the <code class="docutils literal notranslate"><span class="pre">tag:tagname</span></code> syntax:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span></code></p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">--finder-tag-keywords</span></code> also works with <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> as described above in the section on <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span></code></p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--xattr-template</span></code> option allows you to set a variety of other extended attributes. It is used in the format <code class="docutils literal notranslate"><span class="pre">--xattr-template</span> <span class="pre">ATTRIBUTE</span> <span class="pre">TEMPLATE</span></code> where ATTRIBUTE is one of ‘authors’,’comment’, ‘copyright’, ‘description’, ‘findercomment’, ‘headline’, ‘keywords’.</p>
|
||||
<p>For example, to set Finder comment to the photo’s title and description:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{newline}{descr}"</span></code></p>
|
||||
<p>In the template string above, <code class="docutils literal notranslate"><span class="pre">{newline}</span></code> instructs osxphotos to insert a new line character (“n”) between the title and description. In this example, if <code class="docutils literal notranslate"><span class="pre">{title}</span></code> or <code class="docutils literal notranslate"><span class="pre">{descr}</span></code> is empty, you’ll get “titlen” or “ndescription” which may not be desired so you can use more advanced features of the template system to handle these cases:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{title?{descr?{newline},},}{descr}"</span></code></p>
|
||||
<p>Explanation of the template string:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>{title}{title?{descr?{newline},},}{descr}
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──> insert title │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└───> is there a title?
|
||||
│ │ │ │ │
|
||||
└───> if so, is there a description?
|
||||
│ │ │ │
|
||||
└───> if so, insert new line
|
||||
│ │ │
|
||||
└───> if descr is blank, insert nothing
|
||||
│ │
|
||||
└───> if title is blank, insert nothing
|
||||
│
|
||||
└───> finally, insert description
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>In this example, <code class="docutils literal notranslate"><span class="pre">title?</span></code> demonstrates use of the boolean (True/False) feature of the template system. <code class="docutils literal notranslate"><span class="pre">title?</span></code> is read as “Is the title True (or not blank/empty)? If so, then the value immediately following the <code class="docutils literal notranslate"><span class="pre">?</span></code> is used in place of <code class="docutils literal notranslate"><span class="pre">title</span></code>. If <code class="docutils literal notranslate"><span class="pre">title</span></code> is blank, then the value immediately following the comma is used instead. The format for boolean fields is <code class="docutils literal notranslate"><span class="pre">field?value</span> <span class="pre">if</span> <span class="pre">true,value</span> <span class="pre">if</span> <span class="pre">false</span></code>. Either <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">true</span></code> or <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">false</span></code> may be blank, in which case a blank string (“”) is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex <code class="docutils literal notranslate"><span class="pre">if-then-else</span></code> statements.</p>
|
||||
<p>The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.</p>
|
||||
<p>See Extended Attributes section in the help for <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> for additional information about this feature.</p>
|
||||
</div>
|
||||
<div class="section" id="saving-and-loading-options">
|
||||
<h1>Saving and loading options<a class="headerlink" href="#saving-and-loading-options" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (<code class="docutils literal notranslate"><span class="pre">--save-config</span> <span class="pre">FILE</span></code>) and then load them (<code class="docutils literal notranslate"><span class="pre">--load-config</span> <span class="pre">FILE</span></code>) instead of repeating each option on the command line.</p>
|
||||
<p>To save the configuration:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre"><all</span> <span class="pre">your</span> <span class="pre">options</span> <span class="pre">here></span> <span class="pre">--update</span> <span class="pre">--save-config</span> <span class="pre">osxphotos.toml</span></code></p>
|
||||
<p>Then the next to you run osxphotos, you can simply do this:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--load-config</span> <span class="pre">osxphotos.toml</span></code></p>
|
||||
<p>The configuration file is a plain text file in <a class="reference external" href="https://toml.io/en/">TOML</a> format so the <code class="docutils literal notranslate"><span class="pre">.toml</span></code> extension is standard but you can name the file anything you like.</p>
|
||||
</div>
|
||||
<div class="section" id="an-example-from-an-actual-osxphotos-user">
|
||||
<h1>An example from an actual osxphotos user<a class="headerlink" href="#an-example-from-an-actual-osxphotos-user" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Here’s a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>I usually import my iPhone’s photo roll on a more or less regular basis, and it
|
||||
includes photos and videos. As a result, the size ot my Photos library may rise
|
||||
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
|
||||
quite good keyword management system.
|
||||
|
||||
After a while, I want to take most of the videos out of the library and move them
|
||||
to a separate "videos" folder on a different folder / volume. As I might want to
|
||||
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
|
||||
its internal library tagging system, I will use osxphotos to do just this.
|
||||
|
||||
Picking the videos can be left to Photos, using a smart folder for instance. Then
|
||||
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
|
||||
to spot all videos created on my iPhone using the Quik application (now part of
|
||||
GoPro).
|
||||
|
||||
I want to retrieve my keywords only and make sure they populate the Finder tags, as
|
||||
well as export all the persons identified in the videos by Photos. I also want to
|
||||
merge any keywords or persons already in the video metadata with the exported
|
||||
metadata.
|
||||
|
||||
Keeping Photo’s edited titles and descriptions and putting both in the Finder
|
||||
comments field in a readable manner is also enabled.
|
||||
|
||||
And I want to keep the file’s creation date (using `--touch-file`).
|
||||
|
||||
Finally, use `--strip` to remove any leading or trailing whitespace from processed
|
||||
template fields.
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/folder</span> <span class="pre">for</span> <span class="pre">exported</span> <span class="pre">videos/</span> <span class="pre">--keyword</span> <span class="pre">Quik</span> <span class="pre">--only-movies</span> <span class="pre">--db</span> <span class="pre">/path</span> <span class="pre">to</span> <span class="pre">my.photoslibrary</span> <span class="pre">--touch-file</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--person-keyword</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{title?{descr?{newline},},}{descr}"</span> <span class="pre">--exiftool-merge-keywords</span> <span class="pre">--exiftool-merge-persons</span> <span class="pre">--exiftool</span> <span class="pre">--strip</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="conclusion">
|
||||
<h1>Conclusion<a class="headerlink" href="#conclusion" title="Permalink to this headline">¶</a></h1>
|
||||
<p>osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="index.html">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="index.html">Documentation overview</a><ul>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/tutorial.md.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
354
docsrc/source/tutorial.md
Normal file
354
docsrc/source/tutorial.md
Normal file
@@ -0,0 +1,354 @@
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
|
||||
# OSXPhotos Tutorial
|
||||
|
||||
## Tutorial
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:END -->
|
||||
|
||||
The design philosophy for osxphotos is "make the easy things easy and make the hard things possible". To "make the hard things possible", osxphotos is very flexible and has many, many configuration options -- the `export` command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the `export` command which exports photos from the library so that's the focus of this tutorial.
|
||||
|
||||
### Export your photos
|
||||
|
||||
`osxphotos export /path/to/export`
|
||||
|
||||
This command exports all your photos to the `/path/to/export` directory.
|
||||
|
||||
**Note**: osxphotos uses the term 'photo' to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple "Live Photo" which is an image and an associated "live preview" video file), a JPEG image with an associated RAW image, etc.
|
||||
|
||||
### Export by date
|
||||
|
||||
While the previous command will export all your photos (and videos--see note above), it probably doesn't do exactly what you want. In the previous example, all the photos will be exported to a single folder: `/path/to/export`. If you have a large library with thousands of images and videos, this likely isn't very useful. You can use the `--export-by-date` option to export photos to a folder structure organized by year, month, day, e.g. `2021/04/21`:
|
||||
|
||||
`osxphotos export /path/to/export --export-by-date`
|
||||
|
||||
With this command, a photo that was created on 31 May 2015 would be exported to: `/path/to/export/2015/05/31`
|
||||
|
||||
### Specify directory structure
|
||||
|
||||
If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START -->template system<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --> that allows you to specify the directory structure using the `--directory` option. For example, this command exported to a directory structure that looks like: `2015/May` (4-digit year / month name):
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{created.month}"`
|
||||
|
||||
The string following `--directory` is an `osxphotos template string`. Template strings are widely used throughout osxphotos and it's worth your time to learn more about them. In a template string, the values between the curly braces, e.g. `{created.year}` are replaced with metadata from the photo being exported. In this case, `{created.year}` is the 4-digit year of the photo's creation date and `{created.month}` is the full month name in the user's locale (e.g. `May`, `mai`, etc.). In the osxphotos template system these are referred to as template fields. The text not included between `{}` pairs is interpreted literally, in this case `/`, is a directory separator.
|
||||
|
||||
osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the `--directory` template, you could thus export photos organized by country name:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country}"`
|
||||
|
||||
Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country,No-Country}"`
|
||||
|
||||
The value after the ',' in the template string is the default value, in this case 'No-Country'. **Note**: If you don't specify a default value and a template field is null, osxphotos will use "_" (underscore character) as the default.
|
||||
|
||||
Some template fields, such as `{keyword}`, may expand to more than one value. For example, if a photo has keywords of "Travel" and "Vacation", `{keyword}` would expand to "Travel", "Vacation". When used with `--directory`, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{keyword}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel/IMG_1234.JPG
|
||||
/path/to/export/Vacation/IMG_1234.JPG
|
||||
|
||||
### Specify exported filename
|
||||
|
||||
By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like `IMG_1234.JPG` or `DSC05678.dng`. osxphotos allows you to specify a custom filename template using the `--filename` option in the same way as `--directory` allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{title}"`
|
||||
|
||||
The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{title,{original_name}}"
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
Use photo's title as the filename <──────┘ ││ │
|
||||
││ │
|
||||
Value after comma will be used <───────┘│ │
|
||||
if title is blank │ │
|
||||
│ │
|
||||
The default value can be <────┘ │
|
||||
another template field │
|
||||
│
|
||||
Use photo's original name if no title <──────┘
|
||||
```
|
||||
|
||||
The osxphotos template system also allows for limited conditional logic of the type "If a condition is true then do one thing, otherwise, do a different thing". For example, you can use the `--filename` option to name files that are marked as "Favorites" in Photos differently than other files. For example, to add a "#" to the name of every photo that's a favorite:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
|
||||
│ │ │││
|
||||
│ │ │││
|
||||
Use photo's original name as filename <──┘ │ │││
|
||||
│ │││
|
||||
'favorite' is True if photo is a Favorite, <───────┘ │││
|
||||
otherwise, False │││
|
||||
│││
|
||||
'?' specifies a conditional <─────────────┘││
|
||||
││
|
||||
Value immediately following ? will be used if <──────┘│
|
||||
preceding template field is True or non-blank │
|
||||
│
|
||||
Value immediately following comma will be used if <──────┘
|
||||
template field is False or blank (null); in this case
|
||||
no value is specified so a blank string "" will be used
|
||||
```
|
||||
|
||||
Like with `--directory`, using a multi-valued template field such as `{keyword}` may result in more than one copy of a photo being exported. For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{keyword}-{original_name}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel-IMG_1234.JPG
|
||||
/path/to/export/Vacation-IMG_1234.JPG
|
||||
|
||||
### Edited photos
|
||||
|
||||
If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append "_edited" to the edited image. For example, if the original image was named `IMG_1234.JPG`, osxphotos will export the original as `IMG_1234.JPG` and the edited version as `IMG_1234_edited.jpeg`. **Note:** Photos changes the extension of edited images to ".jpeg" even if the original was named ".JPG". You can change the suffix appended to edited images using the `--edited-suffix` option:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_EDIT"`
|
||||
|
||||
In this example, the edited image would be named `IMG_1234_EDIT.jpeg`. Like many options in osxphotos, the `--edited-suffix` option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_{modified.year}-{modified.mm}-{modified.dd}"`
|
||||
|
||||
In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: `IMG_1234_2021-04-21.jpeg`.
|
||||
|
||||
You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using `--skip-edited`:
|
||||
|
||||
`osxphotos export /path/to/export --skip-edited`
|
||||
|
||||
You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the `--skip-original-if-edited` option:
|
||||
|
||||
`osxphotos export /path/to/export --skip-original-if-edited`
|
||||
|
||||
As mentioned above, Photos renames JPEG images that have been edited with the ".jpeg" extension. Some applications use ".JPG" and others use ".jpg" or ".JPEG". You can use the `--jpeg-ext` option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. `--jpeg-ext jpg` to use '.jpg' for all JPEGs.
|
||||
|
||||
`osxphotos export /path/to/export --jpeg-ext jpg`
|
||||
|
||||
### Specifying the Photos library
|
||||
|
||||
All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the "Option" key while opening Photos, you can select an alternate Photos library. If you don't specify which library to use, osxphotos will try find the last opened library. Occasionally it can't determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the `--db` option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).
|
||||
|
||||
`osxphotos export /path/to/export --db ~/Pictures/MyAlternateLibrary.photoslibrary`
|
||||
|
||||
### Missing photos
|
||||
|
||||
osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.
|
||||
|
||||
If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the `--download-missing` option. `--download-missing` uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos' AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There's also an experimental `--use-photokit` option that will communicate with Photos using a different "PhotoKit" interface. This option must be used together with `--download-missing`:
|
||||
|
||||
`osxphotos export /path/to/export --download-missing`
|
||||
|
||||
`osxphotos export /path/to/export --download-missing --use-photokit`
|
||||
|
||||
### Exporting to external disks
|
||||
|
||||
If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the `--retry` option so that osxphotos will automatically retry the export. Use `--retry` with a number that specifies the number of times to retry the export:
|
||||
|
||||
`osxphotos export /path/to/export --retry 3`
|
||||
|
||||
In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.
|
||||
|
||||
### Exporting metadata with exported photos
|
||||
|
||||
Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos' native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free [`exiftool`](https://exiftool.org/) app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the `--exiftool` option to write metadata to exported photos:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool`
|
||||
|
||||
This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with `--exiftool` to modify the metadata that is written by `exiftool`. For example, you can use the `--keyword-template` option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
|
||||
│ │
|
||||
│ │
|
||||
folder_album results in the folder(s) <──┘ │
|
||||
and album a photo is contained in │
|
||||
│
|
||||
The value in () is used as the path separator <───────┘
|
||||
for joining the folders and albums. For example,
|
||||
if photo is in Folder1/Folder2/Album, (>) produces
|
||||
"Folder1>Folder2>Album" which some programs, such as
|
||||
Lightroom Classic, treat as hierarchal keywords
|
||||
```
|
||||
|
||||
The above command will write all the regular metadata that `--exiftool` normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form "Folder1>Folder2>Album". If you did not include the `(>)` in the template string (e.g. `{folder_album}`), folder_album would render in form "Folder1/Folder2/Album".
|
||||
|
||||
A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the `{label}`. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image's metadata:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label}"`
|
||||
|
||||
**Note**: When evaluating templates for `--directory` and `--filename`, osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as `--keyword-template`, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"`
|
||||
|
||||
### Sidecar files
|
||||
|
||||
Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the `--sidecar` option. For example, to output metadata to XMP sidecars:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP`
|
||||
|
||||
Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`.
|
||||
|
||||
Sidecar files are named "photoname.ext.sidecar_ext". For example, if the photo is named `IMG_1234.JPG` and the sidecar format is XMP, the sidecar would be named `IMG_1234.JPG.XMP`. Some applications expect the sidecar in this case to be named `IMG_1234.XMP`. You can use the `-sidecar-drop-ext` option to force osxphotos to name the sidecar files in this manner:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP -sidecar-drop-ext`
|
||||
|
||||
### Updating a previous export
|
||||
|
||||
If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the `--update` option. When `osxphotos export` is run, it creates a database file named `.osxphotos_export.db` in the export folder. (**Note** Because the filename starts with a ".", you won't see it in Finder which treats "dot-files" like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the `--update` option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:
|
||||
|
||||
`osxphotos export /path/to/export --update`
|
||||
|
||||
will read the export database located in `/path/to/export/.osxphotos_export.db` and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the `--update` option even if it's never been run before. If the database isn't found, osxphotos will create it. If you run `osxphotos export` without `--update` in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use `--update`.
|
||||
|
||||
If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of `--update`, you can use the `--only-new` with `--update` to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without `--only-new`, osxphotos would see that previously exported files are missing and re-export them.
|
||||
|
||||
`osxphotos export /path/to/export --update --only-new`
|
||||
|
||||
If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with `--update`, you should use the `--ignore-signature` option. `--ignore-signature` instructs osxphotos to ignore the file's signature (for example, size and date modified) when deciding which files should be updated with `--update`. If you edit a file in the export directory and then run `--update` without `--ignore-signature`, osxphotos will see that the file is different than the one in the Photos library and re-export it.
|
||||
|
||||
`osxphotos export /path/to/export --update --ignore-signature`
|
||||
|
||||
### Dry Run
|
||||
|
||||
You can use the `--dry-run` option to have osxphotos "dry run" or test an export without actually exporting any files. When combined with the `--verbose` option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your `--directory` and `--filename` templates are correct, `--dry-run --verbose` will print out the name of each file being exported.
|
||||
|
||||
`osxphotos export /path/to/export --dry-run --verbose`
|
||||
|
||||
### Creating a report of all exported files
|
||||
|
||||
You can use the `--report` option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the `--report` option:
|
||||
|
||||
`osxphotos export /path/to/export --report export.csv`
|
||||
|
||||
### Exporting only certain photos
|
||||
|
||||
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
|
||||
|
||||
For example, to export only photos with keyword `Travel`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel"`
|
||||
|
||||
Like many options in osxphotos, `--keyword` (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword `Travel` *or* keyword `Vacation`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel" --keyword "Vacation"`
|
||||
|
||||
To export only photos contained in the album "Summer Vacation":
|
||||
|
||||
`osxphotos export /path/to/export --album "Summer Vacation"`
|
||||
|
||||
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
|
||||
|
||||
`osxphotos export /path/to/export --portrait`
|
||||
|
||||
You can also export photos in a certain date range:
|
||||
|
||||
`osxphotos export /path/to/export --from-date "2020-01-01" --to-date "2020-02-28"`
|
||||
|
||||
### Converting images to JPEG on export
|
||||
|
||||
Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the `--convert-to-jpeg` option. You can specify the JPEG quality (0: worst, 1.0: best) using `--jpeg-quality`. For example:
|
||||
|
||||
`osxphotos export /path/to/export --convert-to-jpeg --jpeg-quality 0.9`
|
||||
|
||||
### Finder attributes
|
||||
|
||||
In addition to using `exiftool` to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.
|
||||
|
||||
osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the `tag:tagname` syntax:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords`
|
||||
|
||||
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords --keyword-template "{label}"`
|
||||
|
||||
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
|
||||
|
||||
For example, to set Finder comment to the photo's title and description:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{newline}{descr}"`
|
||||
|
||||
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
|
||||
|
||||
Explanation of the template string:
|
||||
|
||||
```txt
|
||||
{title}{title?{descr?{newline},},}{descr}
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──> insert title │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└───> is there a title?
|
||||
│ │ │ │ │
|
||||
└───> if so, is there a description?
|
||||
│ │ │ │
|
||||
└───> if so, insert new line
|
||||
│ │ │
|
||||
└───> if descr is blank, insert nothing
|
||||
│ │
|
||||
└───> if title is blank, insert nothing
|
||||
│
|
||||
└───> finally, insert description
|
||||
```
|
||||
|
||||
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
|
||||
|
||||
The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.
|
||||
|
||||
See Extended Attributes section in the help for `osxphotos export` for additional information about this feature.
|
||||
|
||||
### Saving and loading options
|
||||
|
||||
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
|
||||
|
||||
To save the configuration:
|
||||
|
||||
`osxphotos export /path/to/export <all your options here> --update --save-config osxphotos.toml`
|
||||
|
||||
Then the next to you run osxphotos, you can simply do this:
|
||||
|
||||
`osxphotos export /path/to/export --load-config osxphotos.toml`
|
||||
|
||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||
|
||||
### An example from an actual osxphotos user
|
||||
|
||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||
|
||||
I usually import my iPhone’s photo roll on a more or less regular basis, and it
|
||||
includes photos and videos. As a result, the size ot my Photos library may rise
|
||||
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
|
||||
quite good keyword management system.
|
||||
|
||||
After a while, I want to take most of the videos out of the library and move them
|
||||
to a separate "videos" folder on a different folder / volume. As I might want to
|
||||
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
|
||||
its internal library tagging system, I will use osxphotos to do just this.
|
||||
|
||||
Picking the videos can be left to Photos, using a smart folder for instance. Then
|
||||
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
|
||||
to spot all videos created on my iPhone using the Quik application (now part of
|
||||
GoPro).
|
||||
|
||||
I want to retrieve my keywords only and make sure they populate the Finder tags, as
|
||||
well as export all the persons identified in the videos by Photos. I also want to
|
||||
merge any keywords or persons already in the video metadata with the exported
|
||||
metadata.
|
||||
|
||||
Keeping Photo’s edited titles and descriptions and putting both in the Finder
|
||||
comments field in a readable manner is also enabled.
|
||||
|
||||
And I want to keep the file’s creation date (using `--touch-file`).
|
||||
|
||||
Finally, use `--strip` to remove any leading or trailing whitespace from processed
|
||||
template fields.
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
17
examples/template_filter.py
Normal file
17
examples/template_filter.py
Normal file
@@ -0,0 +1,17 @@
|
||||
""" Example of using a custom python function as an osxphotos template filter
|
||||
|
||||
Use in formath:
|
||||
"{template_field|template_filter.py::myfilter}"
|
||||
|
||||
Your filter function will receive a list of strings even if the template renders to a single value.
|
||||
You should expect a list and return a list and be able to handle multi-value templates like {keyword}
|
||||
as well as single-value templates like {original_name}
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
def myfilter(values: List[str]) -> List[str]:
|
||||
""" Custom filter to append "foo-" to template value """
|
||||
values = ["foo-" + val for val in values]
|
||||
return values
|
||||
|
||||
30
examples/template_function.py
Normal file
30
examples/template_function.py
Normal file
@@ -0,0 +1,30 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template
|
||||
Use: osxphotos export /path/to/export --filename "{function:/path/to/template_function.py::example}"
|
||||
|
||||
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
from typing import List, Union
|
||||
|
||||
import osxphotos
|
||||
|
||||
|
||||
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
||||
""" example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
filename = pathlib.Path(photo.original_filename).stem
|
||||
if photo.hasadjustments:
|
||||
filename += "#"
|
||||
if photo.favorite:
|
||||
filename += "!"
|
||||
|
||||
return filename
|
||||
@@ -3,6 +3,7 @@ from .photoinfo import PhotoInfo
|
||||
from .photosdb import PhotosDB
|
||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||
from .phototemplate import PhotoTemplate
|
||||
from .queryoptions import QueryOptions
|
||||
from .utils import _debug, _get_logger, _set_debug
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
|
||||
@@ -209,4 +209,11 @@ EXTENDED_ATTRIBUTE_NAMES = [
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
||||
|
||||
# name of export DB
|
||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||
|
||||
# bit flags for burst images ("burstPickType")
|
||||
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
|
||||
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
|
||||
BURST_SELECTED = 0b1000 # 8: burst image is selected
|
||||
BURST_KEY = 0b10000 # 16: burst image is the key photo (top of burst stack)
|
||||
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.41.4"
|
||||
__version__ = "0.42.17"
|
||||
|
||||
1331
osxphotos/cli.py
1331
osxphotos/cli.py
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from functools import lru_cache # pylint: disable=syntax-error
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# exiftool -stay_open commands outputs this EOF marker after command is run
|
||||
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
||||
@@ -308,12 +309,13 @@ class ExifTool:
|
||||
ver, _, _ = self.run_commands("-ver", no_file=True)
|
||||
return ver.decode("utf-8")
|
||||
|
||||
def asdict(self, tag_groups=True):
|
||||
def asdict(self, tag_groups=True, normalized=False):
|
||||
"""return dictionary of all EXIF tags and values from exiftool
|
||||
returns empty dict if no tags
|
||||
|
||||
Args:
|
||||
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
|
||||
normalized: if True, dict keys are all normalized to lower case (default is False)
|
||||
"""
|
||||
json_str, _, _ = self.run_commands("-json")
|
||||
if not json_str:
|
||||
@@ -334,6 +336,10 @@ class ExifTool:
|
||||
k = re.sub(r".*:", "", k)
|
||||
exif_new[k] = v
|
||||
exifdict = exif_new
|
||||
|
||||
if normalized:
|
||||
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||
|
||||
return exifdict
|
||||
|
||||
def json(self):
|
||||
@@ -360,3 +366,74 @@ class ExifTool:
|
||||
elif self._commands:
|
||||
# run_commands sets self.warning and self.error as needed
|
||||
self.run_commands(*self._commands)
|
||||
|
||||
|
||||
class ExifToolCaching(ExifTool):
|
||||
""" Basic exiftool interface for reading and writing EXIF tags, with caching.
|
||||
Use this only when you know the file's EXIF data will not be changed by any external process.
|
||||
|
||||
Creates a singleton cached ExifTool instance """
|
||||
|
||||
_singletons = {}
|
||||
|
||||
def __new__(cls, filepath, exiftool=None):
|
||||
""" create new object or return instance of already created singleton """
|
||||
if filepath not in cls._singletons:
|
||||
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
|
||||
return cls._singletons[filepath]
|
||||
|
||||
|
||||
class _ExifToolCaching(ExifTool):
|
||||
def __init__(self, filepath, exiftool=None):
|
||||
"""Create read-only ExifTool object that caches values
|
||||
|
||||
Args:
|
||||
file: path to image file
|
||||
exiftool: path to exiftool, if not specified will look in path
|
||||
|
||||
Returns:
|
||||
ExifTool instance
|
||||
"""
|
||||
self._json_cache = None
|
||||
self._asdict_cache = {}
|
||||
super().__init__(filepath, exiftool=exiftool, overwrite=False, flags=None)
|
||||
|
||||
def run_commands(self, *commands, no_file=False):
|
||||
if commands[0] not in ["-json", "-ver"]:
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
return super().run_commands(*commands, no_file=no_file)
|
||||
|
||||
def setvalue(self, tag, value):
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
|
||||
def addvalues(self, tag, *values):
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
|
||||
def json(self):
|
||||
if not self._json_cache:
|
||||
self._json_cache = super().json()
|
||||
return self._json_cache
|
||||
|
||||
def asdict(self, tag_groups=True, normalized=False):
|
||||
"""return dictionary of all EXIF tags and values from exiftool
|
||||
returns empty dict if no tags
|
||||
|
||||
Args:
|
||||
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
|
||||
normalized: if True, dict keys are all normalized to lower case (default is False)
|
||||
"""
|
||||
try:
|
||||
return self._asdict_cache[tag_groups][normalized]
|
||||
except KeyError:
|
||||
if tag_groups not in self._asdict_cache:
|
||||
self._asdict_cache[tag_groups] = {}
|
||||
self._asdict_cache[tag_groups][normalized] = super().asdict(
|
||||
tag_groups=tag_groups, normalized=normalized
|
||||
)
|
||||
return self._asdict_cache[tag_groups][normalized]
|
||||
|
||||
def flush_cache(self):
|
||||
""" Clear cached data so that calls to json or asdict return fresh data """
|
||||
self._json_cache = None
|
||||
self._asdict_cache = {}
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
from ._photoinfo_exifinfo import ExifInfo
|
||||
from ._photoinfo_export import ExportResults
|
||||
from ._photoinfo_scoreinfo import ScoreInfo
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photoinfo import PhotoInfo
|
||||
@@ -89,6 +89,9 @@ class ExportResults:
|
||||
xattr_skipped=None,
|
||||
deleted_files=None,
|
||||
deleted_directories=None,
|
||||
exported_album=None,
|
||||
skipped_album=None,
|
||||
missing_album=None,
|
||||
):
|
||||
self.exported = exported or []
|
||||
self.new = new or []
|
||||
@@ -111,6 +114,9 @@ class ExportResults:
|
||||
self.xattr_skipped = xattr_skipped or []
|
||||
self.deleted_files = deleted_files or []
|
||||
self.deleted_directories = deleted_directories or []
|
||||
self.exported_album = exported_album or []
|
||||
self.skipped_album = skipped_album or []
|
||||
self.missing_album = missing_album or []
|
||||
|
||||
def all_files(self):
|
||||
""" return all filenames contained in results """
|
||||
@@ -157,6 +163,10 @@ class ExportResults:
|
||||
self.exiftool_error += other.exiftool_error
|
||||
self.deleted_files += other.deleted_files
|
||||
self.deleted_directories += other.deleted_directories
|
||||
self.exported_album += other.exported_album
|
||||
self.skipped_album += other.skipped_album
|
||||
self.missing_album += other.missing_album
|
||||
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
@@ -181,6 +191,9 @@ class ExportResults:
|
||||
+ f",exiftool_error={self.exiftool_error}"
|
||||
+ f",deleted_files={self.deleted_files}"
|
||||
+ f",deleted_directories={self.deleted_directories}"
|
||||
+ f",exported_album={self.exported_album}"
|
||||
+ f",skipped_album={self.skipped_album}"
|
||||
+ f",missing_album={self.missing_album}"
|
||||
+ ")"
|
||||
)
|
||||
|
||||
@@ -621,7 +634,11 @@ def export2(
|
||||
)
|
||||
edited_name = pathlib.Path(self.path_edited).name
|
||||
edited_suffix = pathlib.Path(edited_name).suffix
|
||||
fname = pathlib.Path(self.original_filename).stem + edited_identifier + edited_suffix
|
||||
fname = (
|
||||
pathlib.Path(self.original_filename).stem
|
||||
+ edited_identifier
|
||||
+ edited_suffix
|
||||
)
|
||||
else:
|
||||
fname = self.original_filename
|
||||
|
||||
@@ -1654,13 +1671,13 @@ def _exiftool_dict(
|
||||
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
|
||||
self.date_modified
|
||||
).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]
|
||||
exif[field] = [str(v).replace("\n", " ") for v in val if v is not None]
|
||||
return exif
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ from .._constants import (
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
BURST_DEFAULT_PICK,
|
||||
BURST_KEY,
|
||||
BURST_NOT_SELECTED,
|
||||
BURST_SELECTED,
|
||||
)
|
||||
from ..adjustmentsinfo import AdjustmentsInfo
|
||||
from ..albuminfo import AlbumInfo, ImportInfo
|
||||
@@ -453,9 +457,22 @@ class PhotoInfo:
|
||||
)
|
||||
return self._albums
|
||||
|
||||
@property
|
||||
def burst_albums(self):
|
||||
"""If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums """
|
||||
try:
|
||||
return self._burst_albums
|
||||
except AttributeError:
|
||||
burst_albums = list(self.albums)
|
||||
for photo in self.burst_photos:
|
||||
if photo.burst_key:
|
||||
burst_albums.extend(photo.albums)
|
||||
self._burst_albums = list(set(burst_albums))
|
||||
return self._burst_albums
|
||||
|
||||
@property
|
||||
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:
|
||||
return self._album_info
|
||||
except AttributeError:
|
||||
@@ -465,6 +482,19 @@ class PhotoInfo:
|
||||
]
|
||||
return self._album_info
|
||||
|
||||
@property
|
||||
def burst_album_info(self):
|
||||
""" If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info. """
|
||||
try:
|
||||
return self._burst_album_info
|
||||
except AttributeError:
|
||||
burst_album_info = list(self.album_info)
|
||||
for photo in self.burst_photos:
|
||||
if photo.burst_key:
|
||||
burst_album_info.extend(photo.album_info)
|
||||
self._burst_album_info = list(set(burst_album_info))
|
||||
return self._burst_album_info
|
||||
|
||||
@property
|
||||
def import_info(self):
|
||||
""" ImportInfo object representing import session for the photo or None if no import session """
|
||||
@@ -574,6 +604,23 @@ class PhotoInfo:
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def date_added(self):
|
||||
""" Date photo was added to the database """
|
||||
try:
|
||||
return self._date_added
|
||||
except AttributeError:
|
||||
added_date = self._info["added_date"]
|
||||
if added_date:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
self._date_added = added_date.astimezone(tz=tz)
|
||||
else:
|
||||
self._date_added = None
|
||||
|
||||
return self._date_added
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
@@ -680,6 +727,21 @@ class PhotoInfo:
|
||||
""" Returns True if photo is part of a Burst photo set, otherwise False """
|
||||
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 bool(self._info["burstPickType"] & BURST_SELECTED)
|
||||
|
||||
@property
|
||||
def burst_key(self):
|
||||
""" Returns True if photo is a burst photo and is the key image for the burst set (the image that Photos shows on top of the burst stack), otherwise False """
|
||||
return bool(self._info["burstPickType"] & BURST_KEY)
|
||||
|
||||
@property
|
||||
def burst_default_pick(self):
|
||||
""" Returns True if photo is a burst image and is the photo that Photos selected as the default image for the burst set, otherwise False """
|
||||
return bool(self._info["burstPickType"] & BURST_DEFAULT_PICK)
|
||||
|
||||
@property
|
||||
def burst_photos(self):
|
||||
"""If photo is a burst photo, returns list of PhotoInfo objects
|
||||
@@ -892,6 +954,7 @@ class PhotoInfo:
|
||||
filename=False,
|
||||
dirname=False,
|
||||
strip=False,
|
||||
edited=False,
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
@@ -907,6 +970,7 @@ class PhotoInfo:
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
strip: if True, strips leading/trailing white space from resulting template
|
||||
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -921,6 +985,7 @@ class PhotoInfo:
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
strip=strip,
|
||||
edited_version=edited,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1227,7 +1227,7 @@ class PhotoLibrary:
|
||||
|
||||
Args:
|
||||
burstid: str, burst UUID
|
||||
all: return all burst assets; if False returns only those selected by the user
|
||||
all: return all burst assets; if False returns only those selected by the user (including the "key photo" even if user hasn't manually selected it)
|
||||
|
||||
Returns:
|
||||
list of PhotoAsset objects
|
||||
|
||||
36
osxphotos/photosalbum.py
Normal file
36
osxphotos/photosalbum.py
Normal file
@@ -0,0 +1,36 @@
|
||||
""" PhotosAlbum class to create an album in default Photos library and add photos to it """
|
||||
|
||||
from typing import Optional, List
|
||||
import photoscript
|
||||
from .photoinfo import PhotoInfo
|
||||
from .utils import noop
|
||||
|
||||
|
||||
class PhotosAlbum:
|
||||
def __init__(self, name: str, verbose: Optional[callable] = None):
|
||||
self.name = name
|
||||
self.verbose = verbose or noop
|
||||
self.library = photoscript.PhotosLibrary()
|
||||
|
||||
album = self.library.album(name)
|
||||
if album is None:
|
||||
self.verbose(f"Creating Photos album '{self.name}'")
|
||||
album = self.library.create_album(name)
|
||||
self.album = album
|
||||
|
||||
def add(self, photo: PhotoInfo):
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
self.album.add([photo_])
|
||||
self.verbose(
|
||||
f"Added {photo.original_filename} ({photo.uuid}) to album {self.name}"
|
||||
)
|
||||
|
||||
def add_list(self, photo_list: List[PhotoInfo]):
|
||||
photos = [photoscript.Photo(p.uuid) for p in photo_list]
|
||||
self.album.add(photos)
|
||||
photo_len = len(photos)
|
||||
photo_word = "photos" if photo_len > 1 else "photo"
|
||||
self.verbose(f"Added {photo_len} {photo_word} to album {self.name}")
|
||||
|
||||
def photos(self):
|
||||
return self.album.photos()
|
||||
@@ -8,10 +8,14 @@ import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pprint import pformat
|
||||
from typing import List
|
||||
|
||||
import bitmath
|
||||
|
||||
from .._constants import (
|
||||
_DB_TABLE_NAMES,
|
||||
@@ -29,6 +33,8 @@ from .._constants import (
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_TESTED_OS_VERSIONS,
|
||||
_UNKNOWN_PERSON,
|
||||
BURST_KEY,
|
||||
BURST_SELECTED,
|
||||
TIME_DELTA,
|
||||
)
|
||||
from .._version import __version__
|
||||
@@ -37,6 +43,7 @@ from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||
from ..fileutil import FileUtil
|
||||
from ..personinfo import PersonInfo
|
||||
from ..photoinfo import PhotoInfo
|
||||
from ..queryoptions import QueryOptions
|
||||
from ..utils import (
|
||||
_check_file_exists,
|
||||
_db_is_locked,
|
||||
@@ -893,7 +900,8 @@ class PhotosDB:
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
RKMaster.fileIsReference,
|
||||
RKMaster.importGroupUuid
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -924,7 +932,8 @@ class PhotosDB:
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
RKMaster.fileIsReference,
|
||||
RKMaster.importGroupUuid
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -974,6 +983,7 @@ class PhotosDB:
|
||||
# 41 RKVersion.inTrashDate
|
||||
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
|
||||
# 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)
|
||||
# 44 RKMaster.importGroupUuid -- to get date added from RKImportGroup
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1062,18 +1072,9 @@ class PhotosDB:
|
||||
if burst_uuid not in self._dbphotos_burst:
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid].add(uuid)
|
||||
if row[24] != 2 and row[24] != 4:
|
||||
self._dbphotos[uuid][
|
||||
"burst_key"
|
||||
] = True # it's a key photo (selected from the burst)
|
||||
else:
|
||||
self._dbphotos[uuid][
|
||||
"burst_key"
|
||||
] = False # it's a burst photo but not one that's selected
|
||||
else:
|
||||
# not a burst photo
|
||||
self._dbphotos[uuid]["burst"] = False
|
||||
self._dbphotos[uuid]["burst_key"] = None
|
||||
|
||||
# RKVersion.specialType
|
||||
# 1 == panorama
|
||||
@@ -1176,7 +1177,7 @@ class PhotosDB:
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = row[44]
|
||||
self._dbphotos[uuid]["fok_import_session"] = None
|
||||
|
||||
# get additional details from RKMaster, needed for RAW processing
|
||||
@@ -1366,11 +1367,17 @@ class PhotosDB:
|
||||
|
||||
# get the place data
|
||||
place_data = c.execute(
|
||||
"SELECT modelID, defaultName, type, area " "FROM RKPlace "
|
||||
"SELECT modelID, defaultName, type, area FROM RKPlace"
|
||||
).fetchall()
|
||||
places = {p[0]: p for p in place_data}
|
||||
self._db_places = places
|
||||
|
||||
# get import data
|
||||
import_data = c.execute(
|
||||
"SELECT modelID, uuid, name, importDate from RKImportGroup"
|
||||
).fetchall()
|
||||
self._db_import_group = {i[1]: i for i in import_data}
|
||||
|
||||
for uuid in self._dbphotos:
|
||||
# get placeId which is then used to lookup defaultName
|
||||
place_ids_query = c.execute(
|
||||
@@ -1404,6 +1411,17 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["placeNames"] = place_names
|
||||
self._dbphotos[uuid]["reverse_geolocation"] = None # Photos 5
|
||||
|
||||
# add date added
|
||||
try:
|
||||
import_session = self._db_import_group[
|
||||
self._dbphotos[uuid]["import_uuid"]
|
||||
]
|
||||
self._dbphotos[uuid]["added_date"] = datetime.fromtimestamp(
|
||||
import_session[3] + TIME_DELTA
|
||||
)
|
||||
except (ValueError, TypeError, KeyError):
|
||||
self._dbphotos[uuid]["added_date"] = datetime(1970, 1, 1)
|
||||
|
||||
# build album_titles dictionary
|
||||
for album_id in self._dbalbum_details:
|
||||
title = self._dbalbum_details[album_id]["title"]
|
||||
@@ -1828,7 +1846,6 @@ class PhotosDB:
|
||||
|
||||
# get details about photos
|
||||
verbose("Processing photo details.")
|
||||
logging.debug(f"Getting information about photos")
|
||||
c.execute(
|
||||
f"""SELECT {asset_table}.ZUUID,
|
||||
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
|
||||
@@ -1870,7 +1887,8 @@ class PhotosDB:
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP,
|
||||
{asset_table}.ZVISIBILITYSTATE,
|
||||
{asset_table}.ZTRASHEDDATE,
|
||||
{asset_table}.ZSAVEDASSETTYPE
|
||||
{asset_table}.ZSAVEDASSETTYPE,
|
||||
{asset_table}.ZADDEDDATE
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1918,6 +1936,7 @@ class PhotosDB:
|
||||
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
|
||||
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
|
||||
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
||||
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -2006,18 +2025,9 @@ class PhotosDB:
|
||||
if burst_uuid not in self._dbphotos_burst:
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid].add(uuid)
|
||||
if row[20] != 2 and row[20] != 4:
|
||||
info[
|
||||
"burst_key"
|
||||
] = True # it's a key photo (selected from the burst)
|
||||
else:
|
||||
info[
|
||||
"burst_key"
|
||||
] = False # it's a burst photo but not one that's selected
|
||||
else:
|
||||
# not a burst photo
|
||||
info["burst"] = False
|
||||
info["burst_key"] = None
|
||||
|
||||
# Info on sub-type (live photo, panorama, etc)
|
||||
# ZGENERICASSET.ZKINDSUBTYPE
|
||||
@@ -2106,6 +2116,11 @@ class PhotosDB:
|
||||
info["saved_asset_type"] = row[40]
|
||||
info["isreference"] = row[40] == 10
|
||||
|
||||
try:
|
||||
info["added_date"] = datetime.fromtimestamp(row[41] + TIME_DELTA)
|
||||
except (ValueError, TypeError):
|
||||
info["added_date"] = datetime(1970, 1, 1)
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
@@ -2736,8 +2751,6 @@ class PhotosDB:
|
||||
# an empty album will be in _dbalbum_titles but not _dbalbums_album
|
||||
pass
|
||||
album_set.update(title_set)
|
||||
else:
|
||||
logging.debug(f"Could not find album '{album}' in database")
|
||||
photos_sets.append(album_set)
|
||||
|
||||
if uuid:
|
||||
@@ -2745,8 +2758,6 @@ class PhotosDB:
|
||||
for u in uuid:
|
||||
if u in self._dbphotos:
|
||||
uuid_set.update([u])
|
||||
else:
|
||||
logging.debug(f"Could not find uuid '{u}' in database")
|
||||
photos_sets.append(uuid_set)
|
||||
|
||||
if keywords:
|
||||
@@ -2754,8 +2765,6 @@ class PhotosDB:
|
||||
for keyword in keywords:
|
||||
if keyword in self._dbkeywords_keyword:
|
||||
keyword_set.update(self._dbkeywords_keyword[keyword])
|
||||
else:
|
||||
logging.debug(f"Could not find keyword '{keyword}' in database")
|
||||
photos_sets.append(keyword_set)
|
||||
|
||||
if persons:
|
||||
@@ -2768,8 +2777,6 @@ class PhotosDB:
|
||||
except KeyError:
|
||||
# some persons have zero photos so they won't be in _dbfaces_pk
|
||||
pass
|
||||
else:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
|
||||
if from_date or to_date: # sourcery off
|
||||
@@ -2780,14 +2787,10 @@ class PhotosDB:
|
||||
dsel = {
|
||||
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 not datetime_has_tz(to_date):
|
||||
to_date = datetime_naive_to_local(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()))
|
||||
|
||||
photoinfo = []
|
||||
@@ -2795,7 +2798,10 @@ class PhotosDB:
|
||||
# get the intersection of each argument/search criteria
|
||||
for p in set.intersection(*photos_sets):
|
||||
# filter for non-selected burst photos
|
||||
if self._dbphotos[p]["burst"] and not self._dbphotos[p]["burst_key"]:
|
||||
if self._dbphotos[p]["burst"] and not (
|
||||
self._dbphotos[p]["burstPickType"] & BURST_SELECTED
|
||||
or self._dbphotos[p]["burstPickType"] & BURST_KEY
|
||||
):
|
||||
# not a key/selected burst photo, don't include in returned results
|
||||
continue
|
||||
|
||||
@@ -2846,6 +2852,359 @@ class PhotosDB:
|
||||
pass
|
||||
return photos
|
||||
|
||||
def query(self, options: QueryOptions) -> List[PhotoInfo]:
|
||||
"""Run a query against PhotosDB to extract the photos based on user supplied options
|
||||
|
||||
Args:
|
||||
options: a QueryOptions instance
|
||||
"""
|
||||
|
||||
if options.deleted or options.deleted_only:
|
||||
photos = self.photos(
|
||||
uuid=options.uuid,
|
||||
images=options.photos,
|
||||
movies=options.movies,
|
||||
from_date=options.from_date,
|
||||
to_date=options.to_date,
|
||||
intrash=True,
|
||||
)
|
||||
else:
|
||||
photos = []
|
||||
|
||||
if not options.deleted_only:
|
||||
photos += self.photos(
|
||||
uuid=options.uuid,
|
||||
images=options.photos,
|
||||
movies=options.movies,
|
||||
from_date=options.from_date,
|
||||
to_date=options.to_date,
|
||||
)
|
||||
|
||||
person = normalize_unicode(options.person)
|
||||
keyword = normalize_unicode(options.keyword)
|
||||
album = normalize_unicode(options.album)
|
||||
folder = normalize_unicode(options.folder)
|
||||
title = normalize_unicode(options.title)
|
||||
description = normalize_unicode(options.description)
|
||||
place = normalize_unicode(options.place)
|
||||
label = normalize_unicode(options.label)
|
||||
name = normalize_unicode(options.name)
|
||||
|
||||
if album:
|
||||
photos = _get_photos_by_attribute(
|
||||
photos, "albums", album, options.ignore_case
|
||||
)
|
||||
|
||||
if keyword:
|
||||
photos = _get_photos_by_attribute(
|
||||
photos, "keywords", keyword, options.ignore_case
|
||||
)
|
||||
|
||||
if person:
|
||||
photos = _get_photos_by_attribute(
|
||||
photos, "persons", person, options.ignore_case
|
||||
)
|
||||
|
||||
if label:
|
||||
photos = _get_photos_by_attribute(
|
||||
photos, "labels", label, options.ignore_case
|
||||
)
|
||||
|
||||
if folder:
|
||||
# search for photos in an album in folder
|
||||
# finds photos that have albums whose top level folder matches folder
|
||||
photo_list = []
|
||||
for f in folder:
|
||||
photo_list.extend(
|
||||
[
|
||||
p
|
||||
for p in photos
|
||||
if p.album_info
|
||||
and f
|
||||
in [a.folder_names[0] for a in p.album_info if a.folder_names]
|
||||
]
|
||||
)
|
||||
photos = photo_list
|
||||
|
||||
if title:
|
||||
# search title field for text
|
||||
# if more than one, find photos with all title values in title
|
||||
photo_list = []
|
||||
if options.ignore_case:
|
||||
# case-insensitive
|
||||
for t in title:
|
||||
t = t.lower()
|
||||
photo_list.extend(
|
||||
[p for p in photos if p.title and t in p.title.lower()]
|
||||
)
|
||||
else:
|
||||
for t in title:
|
||||
photo_list.extend([p for p in photos if p.title and t in p.title])
|
||||
photos = photo_list
|
||||
elif options.no_title:
|
||||
photos = [p for p in photos if not p.title]
|
||||
|
||||
if description:
|
||||
# search description field for text
|
||||
# if more than one, find photos with all description values in description
|
||||
photo_list = []
|
||||
if options.ignore_case:
|
||||
# case-insensitive
|
||||
for d in description:
|
||||
d = d.lower()
|
||||
photo_list.extend(
|
||||
[
|
||||
p
|
||||
for p in photos
|
||||
if p.description and d in p.description.lower()
|
||||
]
|
||||
)
|
||||
else:
|
||||
for d in description:
|
||||
photo_list.extend(
|
||||
[p for p in photos if p.description and d in p.description]
|
||||
)
|
||||
photos = photo_list
|
||||
elif options.no_description:
|
||||
photos = [p for p in photos if not p.description]
|
||||
|
||||
if place:
|
||||
# search place.names for text matching place
|
||||
# if more than one place, find photos with all place values in description
|
||||
if options.ignore_case:
|
||||
# case-insensitive
|
||||
for place_name in place:
|
||||
place_name = place_name.lower()
|
||||
photos = [
|
||||
p
|
||||
for p in photos
|
||||
if p.place
|
||||
and any(
|
||||
pname
|
||||
for pname in p.place.names
|
||||
if any(
|
||||
pvalue
|
||||
for pvalue in pname
|
||||
if place_name in pvalue.lower()
|
||||
)
|
||||
)
|
||||
]
|
||||
else:
|
||||
for place_name in place:
|
||||
photos = [
|
||||
p
|
||||
for p in photos
|
||||
if p.place
|
||||
and any(
|
||||
pname
|
||||
for pname in p.place.names
|
||||
if any(pvalue for pvalue in pname if place_name in pvalue)
|
||||
)
|
||||
]
|
||||
elif options.no_place:
|
||||
photos = [p for p in photos if not p.place]
|
||||
|
||||
if options.edited:
|
||||
photos = [p for p in photos if p.hasadjustments]
|
||||
|
||||
if options.external_edit:
|
||||
photos = [p for p in photos if p.external_edit]
|
||||
|
||||
if options.favorite:
|
||||
photos = [p for p in photos if p.favorite]
|
||||
elif options.not_favorite:
|
||||
photos = [p for p in photos if not p.favorite]
|
||||
|
||||
if options.hidden:
|
||||
photos = [p for p in photos if p.hidden]
|
||||
elif options.not_hidden:
|
||||
photos = [p for p in photos if not p.hidden]
|
||||
|
||||
if options.missing:
|
||||
photos = [p for p in photos if not p.path]
|
||||
elif options.not_missing:
|
||||
photos = [p for p in photos if p.path]
|
||||
|
||||
if options.shared:
|
||||
photos = [p for p in photos if p.shared]
|
||||
elif options.not_shared:
|
||||
photos = [p for p in photos if not p.shared]
|
||||
|
||||
if options.shared:
|
||||
photos = [p for p in photos if p.shared]
|
||||
elif options.not_shared:
|
||||
photos = [p for p in photos if not p.shared]
|
||||
|
||||
if options.uti:
|
||||
photos = [p for p in photos if options.uti in p.uti_original]
|
||||
|
||||
if options.burst:
|
||||
photos = [p for p in photos if p.burst]
|
||||
elif options.not_burst:
|
||||
photos = [p for p in photos if not p.burst]
|
||||
|
||||
if options.live:
|
||||
photos = [p for p in photos if p.live_photo]
|
||||
elif options.not_live:
|
||||
photos = [p for p in photos if not p.live_photo]
|
||||
|
||||
if options.portrait:
|
||||
photos = [p for p in photos if p.portrait]
|
||||
elif options.not_portrait:
|
||||
photos = [p for p in photos if not p.portrait]
|
||||
|
||||
if options.screenshot:
|
||||
photos = [p for p in photos if p.screenshot]
|
||||
elif options.not_screenshot:
|
||||
photos = [p for p in photos if not p.screenshot]
|
||||
|
||||
if options.slow_mo:
|
||||
photos = [p for p in photos if p.slow_mo]
|
||||
elif options.not_slow_mo:
|
||||
photos = [p for p in photos if not p.slow_mo]
|
||||
|
||||
if options.time_lapse:
|
||||
photos = [p for p in photos if p.time_lapse]
|
||||
elif options.not_time_lapse:
|
||||
photos = [p for p in photos if not p.time_lapse]
|
||||
|
||||
if options.hdr:
|
||||
photos = [p for p in photos if p.hdr]
|
||||
elif options.not_hdr:
|
||||
photos = [p for p in photos if not p.hdr]
|
||||
|
||||
if options.selfie:
|
||||
photos = [p for p in photos if p.selfie]
|
||||
elif options.not_selfie:
|
||||
photos = [p for p in photos if not p.selfie]
|
||||
|
||||
if options.panorama:
|
||||
photos = [p for p in photos if p.panorama]
|
||||
elif options.not_panorama:
|
||||
photos = [p for p in photos if not p.panorama]
|
||||
|
||||
if options.cloudasset:
|
||||
photos = [p for p in photos if p.iscloudasset]
|
||||
elif options.not_cloudasset:
|
||||
photos = [p for p in photos if not p.iscloudasset]
|
||||
|
||||
if options.incloud:
|
||||
photos = [p for p in photos if p.incloud]
|
||||
elif options.not_incloud:
|
||||
photos = [p for p in photos if not p.incloud]
|
||||
|
||||
if options.has_raw:
|
||||
photos = [p for p in photos if p.has_raw]
|
||||
|
||||
if options.has_comment:
|
||||
photos = [p for p in photos if p.comments]
|
||||
elif options.no_comment:
|
||||
photos = [p for p in photos if not p.comments]
|
||||
|
||||
if options.has_likes:
|
||||
photos = [p for p in photos if p.likes]
|
||||
elif options.no_likes:
|
||||
photos = [p for p in photos if not p.likes]
|
||||
|
||||
if options.is_reference:
|
||||
photos = [p for p in photos if p.isreference]
|
||||
|
||||
if options.in_album:
|
||||
photos = [p for p in photos if p.albums]
|
||||
elif options.not_in_album:
|
||||
photos = [p for p in photos if not p.albums]
|
||||
|
||||
if options.from_time:
|
||||
photos = [p for p in photos if p.date.time() >= options.from_time]
|
||||
|
||||
if options.to_time:
|
||||
photos = [p for p in photos if p.date.time() <= options.to_time]
|
||||
|
||||
if options.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 options.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())
|
||||
|
||||
if name:
|
||||
# search filename fields for text
|
||||
# if more than one, find photos with all title values in filename
|
||||
photo_list = []
|
||||
if options.ignore_case:
|
||||
# case-insensitive
|
||||
for n in name:
|
||||
n = n.lower()
|
||||
photo_list.extend(
|
||||
[
|
||||
p
|
||||
for p in photos
|
||||
if n in p.filename.lower()
|
||||
or n in p.original_filename.lower()
|
||||
]
|
||||
)
|
||||
else:
|
||||
for n in name:
|
||||
photo_list.extend(
|
||||
[
|
||||
p
|
||||
for p in photos
|
||||
if n in p.filename or n in p.original_filename
|
||||
]
|
||||
)
|
||||
photos = photo_list
|
||||
|
||||
if options.min_size:
|
||||
photos = [
|
||||
p
|
||||
for p in photos
|
||||
if bitmath.Byte(p.original_filesize) >= options.min_size
|
||||
]
|
||||
|
||||
if options.max_size:
|
||||
photos = [
|
||||
p
|
||||
for p in photos
|
||||
if bitmath.Byte(p.original_filesize) <= options.max_size
|
||||
]
|
||||
|
||||
if options.regex:
|
||||
flags = re.IGNORECASE if options.ignore_case else 0
|
||||
for regex, template in options.regex:
|
||||
regex = re.compile(regex, flags)
|
||||
photo_list = []
|
||||
for p in photos:
|
||||
rendered, _ = p.render_template(template, none_str="")
|
||||
for value in rendered:
|
||||
if regex.search(value):
|
||||
photo_list.append(p)
|
||||
break
|
||||
photos = photo_list
|
||||
|
||||
if options.query_eval:
|
||||
for q in options.query_eval:
|
||||
query_string = f"[photo for photo in photos if {q}]"
|
||||
try:
|
||||
photos = eval(query_string)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid query_eval CRITERIA: {e}")
|
||||
|
||||
return photos
|
||||
|
||||
def __repr__(self):
|
||||
return f"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"
|
||||
|
||||
@@ -2861,3 +3220,31 @@ class PhotosDB:
|
||||
Includes recently deleted photos and non-selected burst images
|
||||
"""
|
||||
return len(self._dbphotos)
|
||||
|
||||
|
||||
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
|
||||
"""Search for photos based on values being in PhotoInfo.attribute
|
||||
|
||||
Args:
|
||||
photos: a list of PhotoInfo objects
|
||||
attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc)
|
||||
values: list of values to search in property
|
||||
ignore_case: ignore case when searching
|
||||
|
||||
Returns:
|
||||
list of PhotoInfo objects matching search criteria
|
||||
"""
|
||||
photos_search = []
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for x in values:
|
||||
x = x.lower()
|
||||
photos_search.extend(
|
||||
p
|
||||
for p in photos
|
||||
if x in [attr.lower() for attr in getattr(p, attribute)]
|
||||
)
|
||||
else:
|
||||
for x in values:
|
||||
photos_search.extend(p for p in photos if x in getattr(p, attribute))
|
||||
return photos_search
|
||||
|
||||
@@ -4,7 +4,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
|
||||
|
||||
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"`
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
@@ -39,6 +39,7 @@ Valid filters are:
|
||||
- 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]'
|
||||
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||
|
||||
e.g. if Photo keywords are `["FOO","bar"]`:
|
||||
@@ -60,9 +61,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
||||
- `"{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.
|
||||
`[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.
|
||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
- `contains`: template field contains value, similar to python's `in`
|
||||
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
|
||||
- `startswith`: template field starts with value
|
||||
- `endswith`: template field ends with value
|
||||
- `<=`: template field is less than or equal to value
|
||||
- `>=`: template field is greater than or equal to value
|
||||
- `<`: template field is less than value
|
||||
- `>`: template field is greater than value
|
||||
- `==`: template field equals value
|
||||
- `!=`: template field does not equal value
|
||||
|
||||
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
|
||||
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
|
||||
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
|
||||
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
|
||||
|
||||
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
|
||||
|
||||
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example:
|
||||
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean (True/False) 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,
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ from textx import TextXSyntaxError, metamodel_from_file
|
||||
|
||||
from ._constants import _UNKNOWN_PERSON
|
||||
from .datetime_formatter import DateTimeFormatter
|
||||
from .exiftool import ExifTool
|
||||
from .exiftool import ExifToolCaching
|
||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||
from .utils import load_function
|
||||
|
||||
# ensure locale set to user's locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
@@ -47,7 +48,9 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
),
|
||||
"{photo_or_video}": "'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'",
|
||||
"{hdr}": "Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{edited}": "Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{edited}": "True if photo has been edited (has adjustments), otherwise False; use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{edited_version}": "True if template is being rendered for the edited version of a photo, otherwise False. ",
|
||||
"{favorite}": "Photo has been marked as favorite?; True/False value, use in format '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
|
||||
"{created.year}": "4-digit year of photo creation time",
|
||||
"{created.yy}": "2-digit year of photo creation time",
|
||||
@@ -119,6 +122,7 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
||||
"{comma}": "A comma: ','",
|
||||
"{semicolon}": "A semicolon: ';'",
|
||||
"{questionmark}": "A question mark: '?'",
|
||||
"{pipe}": "A vertical pipe: '|'",
|
||||
"{openbrace}": "An open brace: '{'",
|
||||
"{closebrace}": "A close brace: '}'",
|
||||
@@ -126,6 +130,10 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{closeparens}": "A close parentheses: ')'",
|
||||
"{openbracket}": "An open bracket: '['",
|
||||
"{closebracket}": "A close bracket: ']'",
|
||||
"{newline}": r"A newline: '\n'",
|
||||
"{lf}": r"A line feed: '\n', alias for {newline}",
|
||||
"{cr}": r"A carriage return: '\r'",
|
||||
"{crlf}": r"a carriage return + line feed: '\r\n'",
|
||||
}
|
||||
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
@@ -134,7 +142,9 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{keyword}": "Keyword(s) assigned to photo",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
||||
"Labels are added automatically by Photos using machine learning algorithms to categorize images. "
|
||||
"These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
||||
"{exiftool}": "Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||
@@ -145,6 +155,15 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{photo}": "Provides direct access to the PhotoInfo object for the photo. "
|
||||
+ "Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. "
|
||||
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
|
||||
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
|
||||
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
"{function}": "Execute a python function from an external file and use return value as template substitution. "
|
||||
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
|
||||
+ "The function will be passed the PhotoInfo object for the photo. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.",
|
||||
}
|
||||
|
||||
FILTER_VALUES = {
|
||||
@@ -156,6 +175,7 @@ FILTER_VALUES = {
|
||||
"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]'",
|
||||
"function": "Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py"
|
||||
}
|
||||
|
||||
# Just the substitutions without the braces
|
||||
@@ -185,6 +205,11 @@ PUNCTUATION = {
|
||||
"closeparens": ")",
|
||||
"openbracket": "[",
|
||||
"closebracket": "]",
|
||||
"questionmark": "?",
|
||||
"newline": "\n",
|
||||
"lf": "\n",
|
||||
"cr": "\r",
|
||||
"crlf": "\r\n",
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +258,9 @@ class PhotoTemplate:
|
||||
# get parser singleton
|
||||
self.parser = PhotoTemplateParser()
|
||||
|
||||
# should {edited_version} render True?
|
||||
self.edited_version = False
|
||||
|
||||
def render(
|
||||
self,
|
||||
template,
|
||||
@@ -243,6 +271,7 @@ class PhotoTemplate:
|
||||
filename=False,
|
||||
dirname=False,
|
||||
strip=False,
|
||||
edited_version=False,
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
@@ -257,6 +286,7 @@ class PhotoTemplate:
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
strip: if True, strips leading/trailing whitespace from rendered templates
|
||||
edited_version: set to True if you want {edited_version} to resolve to True (e.g. exporting edited version of photo)
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -280,6 +310,8 @@ class PhotoTemplate:
|
||||
# empty string
|
||||
return [], []
|
||||
|
||||
self.edited_version = edited_version
|
||||
|
||||
return self._render_statement(
|
||||
model,
|
||||
none_str=none_str,
|
||||
@@ -317,17 +349,6 @@ class PhotoTemplate:
|
||||
unmatched=unmatched,
|
||||
)
|
||||
|
||||
# process find/replace
|
||||
if ts.template and ts.template.findreplace:
|
||||
new_results = []
|
||||
for result in results:
|
||||
for pair in ts.template.findreplace.pairs:
|
||||
find = pair.find or ""
|
||||
repl = pair.replace or ""
|
||||
result = result.replace(find, repl)
|
||||
new_results.append(result)
|
||||
results = new_results
|
||||
|
||||
rendered_strings = results
|
||||
|
||||
if filename:
|
||||
@@ -362,7 +383,7 @@ class PhotoTemplate:
|
||||
if ts.template:
|
||||
# have a template field to process
|
||||
field = ts.template.field
|
||||
if field not in FIELD_NAMES:
|
||||
if field not in FIELD_NAMES and not field.startswith("photo"):
|
||||
unmatched.append(field)
|
||||
return [], unmatched
|
||||
|
||||
@@ -424,6 +445,30 @@ class PhotoTemplate:
|
||||
else:
|
||||
default = []
|
||||
|
||||
# process conditional
|
||||
if ts.template.conditional is not None:
|
||||
operator = ts.template.conditional.operator
|
||||
negation = ts.template.conditional.negation
|
||||
if ts.template.conditional.value is not None:
|
||||
# conditional value is also a TemplateString
|
||||
conditional_value, u = self._render_statement(
|
||||
ts.template.conditional.value,
|
||||
none_str=none_str,
|
||||
path_sep=path_sep,
|
||||
expand_inplace=expand_inplace,
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
)
|
||||
unmatched.extend(u)
|
||||
else:
|
||||
# this shouldn't happen
|
||||
conditional_value = [""]
|
||||
else:
|
||||
operator = None
|
||||
negation = None
|
||||
conditional_value = []
|
||||
|
||||
vals = []
|
||||
if field in SINGLE_VALUE_SUBSTITUTIONS:
|
||||
vals = self.get_template_value(
|
||||
@@ -442,7 +487,15 @@ class PhotoTemplate:
|
||||
vals = self.get_template_value_exiftool(
|
||||
subfield, filename=filename, dirname=dirname
|
||||
)
|
||||
elif field in MULTI_VALUE_SUBSTITUTIONS:
|
||||
elif field == "function":
|
||||
if subfield is None:
|
||||
raise ValueError(
|
||||
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
|
||||
)
|
||||
vals = self.get_template_value_function(
|
||||
subfield, filename=filename, dirname=dirname
|
||||
)
|
||||
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
||||
vals = self.get_template_value_multi(
|
||||
field, path_sep=path_sep, filename=filename, dirname=dirname
|
||||
)
|
||||
@@ -452,14 +505,6 @@ class PhotoTemplate:
|
||||
|
||||
vals = [val for val in vals if val is not None]
|
||||
|
||||
if is_bool:
|
||||
if not vals:
|
||||
vals = default
|
||||
else:
|
||||
vals = bool_val
|
||||
elif not vals:
|
||||
vals = default or [none_str]
|
||||
|
||||
if expand_inplace or delim is not None:
|
||||
sep = delim if delim is not None else inplace_sep
|
||||
vals = [sep.join(sorted(vals))]
|
||||
@@ -467,6 +512,99 @@ class PhotoTemplate:
|
||||
for filter_ in filters:
|
||||
vals = self.get_template_value_filter(filter_, vals)
|
||||
|
||||
# process find/replace
|
||||
if ts.template.findreplace:
|
||||
new_vals = []
|
||||
for val in vals:
|
||||
for pair in ts.template.findreplace.pairs:
|
||||
find = pair.find or ""
|
||||
repl = pair.replace or ""
|
||||
val = val.replace(find, repl)
|
||||
new_vals.append(val)
|
||||
vals = new_vals
|
||||
|
||||
if operator:
|
||||
# have a conditional operator
|
||||
|
||||
def string_test(test_function):
|
||||
""" Perform string comparison using test_function; closure to capture conditional_value, vals, negation """
|
||||
match = False
|
||||
for c in conditional_value:
|
||||
for v in vals:
|
||||
if test_function(v, c):
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
break
|
||||
if (match and not negation) or (negation and not match):
|
||||
return ["True"]
|
||||
else:
|
||||
return []
|
||||
|
||||
def comparison_test(test_function):
|
||||
""" Perform numerical comparisons using test_function; closure to capture conditional_val, vals, negation """
|
||||
if len(vals) != 1 or len(conditional_value) != 1:
|
||||
raise ValueError(
|
||||
f"comparison operators may only be used with a single value: {vals} {conditional_value}"
|
||||
)
|
||||
try:
|
||||
match = (
|
||||
True
|
||||
if test_function(
|
||||
float(vals[0]), float(conditional_value[0])
|
||||
)
|
||||
else False
|
||||
)
|
||||
if (match and not negation) or (negation and not match):
|
||||
return ["True"]
|
||||
else:
|
||||
return []
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"comparison operators may only be used with values that can be converted to numbers: {vals} {conditional_value}"
|
||||
)
|
||||
|
||||
if operator in ["contains", "matches", "startswith", "endswith"]:
|
||||
# process any "or" values separated by "|"
|
||||
temp_values = []
|
||||
for c in conditional_value:
|
||||
temp_values.extend(c.split("|"))
|
||||
conditional_value = temp_values
|
||||
|
||||
if operator == "contains":
|
||||
vals = string_test(lambda v, c: c in v)
|
||||
elif operator == "matches":
|
||||
vals = string_test(lambda v, c: v == c)
|
||||
elif operator == "startswith":
|
||||
vals = string_test(lambda v, c: v.startswith(c))
|
||||
elif operator == "endswith":
|
||||
vals = string_test(lambda v, c: v.endswith(c))
|
||||
elif operator == "==":
|
||||
match = sorted(vals) == sorted(conditional_value)
|
||||
if (match and not negation) or (negation and not match):
|
||||
vals = ["True"]
|
||||
else:
|
||||
vals = []
|
||||
elif operator == "!=":
|
||||
match = sorted(vals) != sorted(conditional_value)
|
||||
if (match and not negation) or (negation and not match):
|
||||
vals = ["True"]
|
||||
else:
|
||||
vals = []
|
||||
elif operator == "<":
|
||||
vals = comparison_test(lambda v, c: v < c)
|
||||
elif operator == "<=":
|
||||
vals = comparison_test(lambda v, c: v <= c)
|
||||
elif operator == ">":
|
||||
vals = comparison_test(lambda v, c: v > c)
|
||||
elif operator == ">=":
|
||||
vals = comparison_test(lambda v, c: v >= c)
|
||||
|
||||
if is_bool:
|
||||
vals = default if not vals else bool_val
|
||||
elif not vals:
|
||||
vals = default or [none_str]
|
||||
|
||||
pre = ts.pre or ""
|
||||
post = ts.post or ""
|
||||
|
||||
@@ -539,6 +677,10 @@ class PhotoTemplate:
|
||||
value = "hdr" if self.photo.hdr else None
|
||||
elif field == "edited":
|
||||
value = "edited" if self.photo.hasadjustments else None
|
||||
elif field == "edited_version":
|
||||
value = "edited_version" if self.edited_version else None
|
||||
elif field == "favorite":
|
||||
value = "favorite" if self.photo.favorite else None
|
||||
elif field == "created.date":
|
||||
value = DateTimeFormatter(self.photo.date).date
|
||||
elif field == "created.year":
|
||||
@@ -782,42 +924,44 @@ class PhotoTemplate:
|
||||
if values and type(values) == list:
|
||||
value = [v.lower() for v in values]
|
||||
else:
|
||||
value = [values.lower()]
|
||||
value = [values.lower()] if values else []
|
||||
elif filter_ == "upper":
|
||||
if values and type(values) == list:
|
||||
value = [v.upper() for v in values]
|
||||
else:
|
||||
value = [values.upper()]
|
||||
value = [values.upper()] if values else []
|
||||
elif filter_ == "strip":
|
||||
if values and type(values) == list:
|
||||
value = [v.strip() for v in values]
|
||||
else:
|
||||
value = [values.strip()]
|
||||
value = [values.strip()] if values else []
|
||||
elif filter_ == "capitalize":
|
||||
if values and type(values) == list:
|
||||
value = [v.capitalize() for v in values]
|
||||
else:
|
||||
value = [values.capitalize()]
|
||||
value = [values.capitalize()] if values else []
|
||||
elif filter_ == "titlecase":
|
||||
if values and type(values) == list:
|
||||
value = [v.title() for v in values]
|
||||
else:
|
||||
value = [values.title()]
|
||||
value = [values.title()] if values else []
|
||||
elif filter_ == "braces":
|
||||
if values and type(values) == list:
|
||||
value = ["{" + v + "}" for v in values]
|
||||
else:
|
||||
value = ["{" + values + "}"]
|
||||
value = ["{" + values + "}"] if values else []
|
||||
elif filter_ == "parens":
|
||||
if values and type(values) == list:
|
||||
value = ["(" + v + ")" for v in values]
|
||||
else:
|
||||
value = ["(" + values + ")"]
|
||||
value = ["(" + values + ")"] if values else []
|
||||
elif filter_ == "brackets":
|
||||
if values and type(values) == list:
|
||||
value = ["[" + v + "]" for v in values]
|
||||
else:
|
||||
value = ["[" + values + "]"]
|
||||
value = ["[" + values + "]"] if values else []
|
||||
elif filter_.startswith("function:"):
|
||||
value = self.get_template_value_filter_function(filter_, values)
|
||||
else:
|
||||
value = []
|
||||
return value
|
||||
@@ -840,7 +984,7 @@ class PhotoTemplate:
|
||||
""" return list of values for a multi-valued template field """
|
||||
values = []
|
||||
if field == "album":
|
||||
values = self.photo.albums
|
||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
||||
elif field == "keyword":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -854,7 +998,11 @@ class PhotoTemplate:
|
||||
elif field == "folder_album":
|
||||
values = []
|
||||
# photos must be in an album to be in a folder
|
||||
for album in self.photo.album_info:
|
||||
if self.photo.burst:
|
||||
album_info = self.photo.burst_album_info
|
||||
else:
|
||||
album_info = self.photo.album_info
|
||||
for album in album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
if dirname:
|
||||
@@ -887,6 +1035,32 @@ class PhotoTemplate:
|
||||
values = (
|
||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||
)
|
||||
elif field.startswith("photo"):
|
||||
# provide access to PhotoInfo object
|
||||
properties = field.split(".")
|
||||
if len(properties) <= 1:
|
||||
raise ValueError(
|
||||
"Missing property in {photo} template. Use in form {photo.property}."
|
||||
)
|
||||
obj = self.photo
|
||||
for i in range(1, len(properties)):
|
||||
property_ = properties[i]
|
||||
try:
|
||||
obj = getattr(obj, property_)
|
||||
if obj is None:
|
||||
break
|
||||
except AttributeError:
|
||||
raise ValueError(
|
||||
"Invalid property for {photo} template: " + f"'{property_}'"
|
||||
)
|
||||
if obj is None:
|
||||
values = []
|
||||
elif isinstance(obj, bool):
|
||||
values = [property_] if obj else []
|
||||
elif isinstance(obj, (str, int, float)):
|
||||
values = [str(obj)]
|
||||
else:
|
||||
values = [val for val in obj]
|
||||
else:
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
@@ -907,13 +1081,13 @@ class PhotoTemplate:
|
||||
if not self.photo.path:
|
||||
return []
|
||||
|
||||
exif = ExifTool(self.photo.path, exiftool=self.exiftool_path)
|
||||
exifdict = exif.asdict()
|
||||
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||
exif = ExifToolCaching(self.photo.path, exiftool=self.exiftool_path)
|
||||
exifdict = exif.asdict(normalized=True)
|
||||
subfield = subfield.lower()
|
||||
if subfield in exifdict:
|
||||
values = exifdict[subfield]
|
||||
values = [values] if not isinstance(values, list) else values
|
||||
values = [str(v) for v in values]
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
@@ -925,6 +1099,66 @@ class PhotoTemplate:
|
||||
|
||||
return values
|
||||
|
||||
def get_template_value_function(self, subfield, filename=None, dirname=None):
|
||||
"""Get template value from external function """
|
||||
|
||||
if "::" not in subfield:
|
||||
raise ValueError(
|
||||
f"SyntaxError: could not parse function name from '{subfield}'"
|
||||
)
|
||||
|
||||
filename, funcname = subfield.split("::")
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise ValueError(f"'{filename}' does not appear to be a file")
|
||||
|
||||
template_func = load_function(filename, funcname)
|
||||
values = template_func(self.photo)
|
||||
|
||||
if not isinstance(values, (str, list)):
|
||||
raise TypeError(
|
||||
f"Invalid return type for function {funcname}: expected str or list"
|
||||
)
|
||||
if type(values) == str:
|
||||
values = [values]
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname:
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
return values
|
||||
|
||||
def get_template_value_filter_function(self, filter_, values):
|
||||
"""Filter template value from external function """
|
||||
|
||||
filter_ = filter_.replace("function:","")
|
||||
|
||||
if "::" not in filter_:
|
||||
raise ValueError(
|
||||
f"SyntaxError: could not parse function name from '{filter_}'"
|
||||
)
|
||||
|
||||
filename, funcname = filter_.split("::")
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise ValueError(f"'{filename}' does not appear to be a file")
|
||||
|
||||
template_func = load_function(filename, funcname)
|
||||
|
||||
if not isinstance(values, (list, tuple)):
|
||||
values = [values]
|
||||
values = template_func(values)
|
||||
|
||||
if not isinstance(values, list):
|
||||
raise TypeError(
|
||||
f"Invalid return type for function {funcname}: expected list"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def get_photo_video_type(self, default):
|
||||
""" return media type, e.g. photo or video """
|
||||
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// OSXPhotos Template Language (OTL)
|
||||
// a TemplateString has format:
|
||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post
|
||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?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
|
||||
@@ -25,6 +25,7 @@ Template:
|
||||
filter=Filter
|
||||
pathsep=PathSep
|
||||
findreplace=FindReplace
|
||||
conditional=Conditional
|
||||
bool=Boolean
|
||||
default=Default
|
||||
"}"
|
||||
@@ -32,7 +33,7 @@ Template:
|
||||
;
|
||||
|
||||
NON_TEMPLATE_STRING:
|
||||
/[^\{\},]*/
|
||||
/[^\{\},\?]*/
|
||||
;
|
||||
|
||||
Delim:
|
||||
@@ -50,6 +51,10 @@ Field:
|
||||
FIELD_WORD+
|
||||
;
|
||||
|
||||
FIELD_WORD:
|
||||
/[\.\w]+/
|
||||
;
|
||||
|
||||
SubField:
|
||||
(
|
||||
":"-
|
||||
@@ -57,12 +62,8 @@ SubField:
|
||||
)?
|
||||
;
|
||||
|
||||
FIELD_WORD:
|
||||
/[\.\w]+/
|
||||
;
|
||||
|
||||
SUBFIELD_WORD:
|
||||
/[\.\w:]+/
|
||||
/[\.\w:\/]+/
|
||||
;
|
||||
|
||||
Filter:
|
||||
@@ -73,7 +74,25 @@ Filter:
|
||||
;
|
||||
|
||||
FILTER_WORD:
|
||||
/[\.\w]+/
|
||||
/[\.\w:\/]+/
|
||||
;
|
||||
|
||||
Conditional:
|
||||
(
|
||||
(" "+)-
|
||||
(negation=NEGATION)?
|
||||
(operator=OPERATOR)
|
||||
(" "+)-
|
||||
(value=Statement)
|
||||
)?
|
||||
;
|
||||
|
||||
NEGATION:
|
||||
"not "
|
||||
;
|
||||
|
||||
OPERATOR:
|
||||
"contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!="
|
||||
;
|
||||
|
||||
PathSep:
|
||||
|
||||
83
osxphotos/queryoptions.py
Normal file
83
osxphotos/queryoptions.py
Normal file
@@ -0,0 +1,83 @@
|
||||
""" QueryOptions class for PhotosDB.query """
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Iterable, Tuple
|
||||
import datetime
|
||||
import bitmath
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryOptions:
|
||||
|
||||
keyword: Optional[Iterable[str]] = None
|
||||
person: Optional[Iterable[str]] = None
|
||||
album: Optional[Iterable[str]] = None
|
||||
folder: Optional[Iterable[str]] = None
|
||||
uuid: Optional[Iterable[str]] = None
|
||||
title: Optional[Iterable[str]] = None
|
||||
no_title: Optional[bool] = None
|
||||
description: Optional[Iterable[str]] = None
|
||||
no_description: Optional[bool] = None
|
||||
ignore_case: Optional[bool] = None
|
||||
edited: Optional[bool] = None
|
||||
external_edit: Optional[bool] = None
|
||||
favorite: Optional[bool] = None
|
||||
not_favorite: Optional[bool] = None
|
||||
hidden: Optional[bool] = None
|
||||
not_hidden: Optional[bool] = None
|
||||
missing: Optional[bool] = None
|
||||
not_missing: Optional[bool] = None
|
||||
shared: Optional[bool] = None
|
||||
not_shared: Optional[bool] = None
|
||||
photos: Optional[bool] = True
|
||||
movies: Optional[bool] = True
|
||||
uti: Optional[Iterable[str]] = None
|
||||
burst: Optional[bool] = None
|
||||
not_burst: Optional[bool] = None
|
||||
live: Optional[bool] = None
|
||||
not_live: Optional[bool] = None
|
||||
cloudasset: Optional[bool] = None
|
||||
not_cloudasset: Optional[bool] = None
|
||||
incloud: Optional[bool] = None
|
||||
not_incloud: Optional[bool] = None
|
||||
from_date: Optional[datetime.datetime] = None
|
||||
to_date: Optional[datetime.datetime] = None
|
||||
from_time: Optional[datetime.time] = None
|
||||
to_time: Optional[datetime.time] = None
|
||||
portrait: Optional[bool] = None
|
||||
not_portrait: Optional[bool] = None
|
||||
screenshot: Optional[bool] = None
|
||||
not_screenshot: Optional[bool] = None
|
||||
slow_mo: Optional[bool] = None
|
||||
not_slow_mo: Optional[bool] = None
|
||||
time_lapse: Optional[bool] = None
|
||||
not_time_lapse: Optional[bool] = None
|
||||
hdr: Optional[bool] = None
|
||||
not_hdr: Optional[bool] = None
|
||||
selfie: Optional[bool] = None
|
||||
not_selfie: Optional[bool] = None
|
||||
panorama: Optional[bool] = None
|
||||
not_panorama: Optional[bool] = None
|
||||
has_raw: Optional[bool] = None
|
||||
place: Optional[Iterable[str]] = None
|
||||
no_place: Optional[bool] = None
|
||||
label: Optional[Iterable[str]] = None
|
||||
deleted: Optional[bool] = None
|
||||
deleted_only: Optional[bool] = None
|
||||
has_comment: Optional[bool] = None
|
||||
no_comment: Optional[bool] = None
|
||||
has_likes: Optional[bool] = None
|
||||
no_likes: Optional[bool] = None
|
||||
is_reference: Optional[bool] = None
|
||||
in_album: Optional[bool] = None
|
||||
not_in_album: Optional[bool] = None
|
||||
burst_photos: Optional[bool] = None
|
||||
missing_bursts: Optional[bool] = None
|
||||
name: Optional[Iterable[str]] = None
|
||||
min_size: Optional[bitmath.Byte] = None
|
||||
max_size: Optional[bitmath.Byte] = None
|
||||
regex: Optional[Iterable[Tuple[str, str]]] = None
|
||||
query_eval: Optional[Iterable[str]] = None
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
@@ -1,5 +1,8 @@
|
||||
""" Utility functions used in osxphotos """
|
||||
|
||||
import fnmatch
|
||||
import glob
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
@@ -13,6 +16,7 @@ import sys
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
from plistlib import load as plistload
|
||||
from typing import Callable
|
||||
|
||||
import CoreFoundation
|
||||
import CoreServices
|
||||
@@ -369,13 +373,16 @@ def _db_is_locked(dbname):
|
||||
|
||||
def normalize_unicode(value):
|
||||
""" normalize unicode data """
|
||||
if value is None:
|
||||
if value is not None:
|
||||
if isinstance(value, (tuple, list)):
|
||||
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
|
||||
elif isinstance(value, str):
|
||||
return unicodedata.normalize(UNICODE_FORMAT, value)
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("value must be str")
|
||||
return unicodedata.normalize(UNICODE_FORMAT, value)
|
||||
|
||||
|
||||
def increment_filename(filepath):
|
||||
""" Return filename (1).ext, etc if filename.ext exists
|
||||
@@ -401,3 +408,28 @@ def increment_filename(filepath):
|
||||
count += 1
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
return str(dest)
|
||||
|
||||
|
||||
def load_function(pyfile: str, function_name: str) -> Callable:
|
||||
""" Load function_name from python file pyfile """
|
||||
module_file = pathlib.Path(pyfile)
|
||||
if not module_file.is_file():
|
||||
raise FileNotFoundError(f"module {pyfile} does not appear to exist")
|
||||
|
||||
module_dir = module_file.parent or pathlib.Path(os.getcwd())
|
||||
module_name = module_file.stem
|
||||
|
||||
# store old sys.path and ensure module_dir at beginning of path
|
||||
syspath = sys.path
|
||||
sys.path = [str(module_dir)] + syspath
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
try:
|
||||
func = getattr(module, function_name)
|
||||
except AttributeError:
|
||||
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
|
||||
finally:
|
||||
# restore sys.path
|
||||
sys.path = syspath
|
||||
|
||||
return func
|
||||
|
||||
@@ -9,6 +9,7 @@ atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
backcall==0.1.0
|
||||
better-exceptions-fork==0.2.1.post6
|
||||
bitmath==1.3.3.1
|
||||
bleach==3.3.0
|
||||
bpylist2==3.0.2
|
||||
certifi==2020.4.5.1
|
||||
@@ -47,7 +48,7 @@ parso==0.6.2
|
||||
pathspec==0.7.0
|
||||
pathvalidate==2.2.1
|
||||
pexpect==4.8.0
|
||||
photoscript==0.1.0
|
||||
photoscript==0.1.2
|
||||
pickleshare==0.7.5
|
||||
Pillow==8.1.1
|
||||
pkginfo==1.5.0.1
|
||||
@@ -55,11 +56,11 @@ pluggy==0.12.0
|
||||
prompt-toolkit==3.0.4
|
||||
psutil==5.7.0
|
||||
ptyprocess==0.6.0
|
||||
py==1.8.0
|
||||
py==1.10.0
|
||||
py2app==0.21
|
||||
pycparser==2.20
|
||||
pyfiglet==0.8.post1
|
||||
Pygments==2.6.1
|
||||
Pygments==2.7.4
|
||||
PyInstaller==3.6
|
||||
pyinstaller-setuptools==2019.3
|
||||
pylint==2.3.1
|
||||
@@ -181,7 +182,7 @@ pyobjc-framework-Vision==6.2.2
|
||||
pyobjc-framework-WebKit==6.2.2
|
||||
pyparsing==2.4.1.1
|
||||
python-dateutil==2.8.1
|
||||
PyYAML==5.1.2
|
||||
PyYAML==5.4
|
||||
pyzmq==18.1.1
|
||||
readme-renderer==25.0
|
||||
regex==2020.2.20
|
||||
|
||||
3
setup.py
3
setup.py
@@ -81,11 +81,12 @@ setup(
|
||||
"pathvalidate==2.2.1",
|
||||
"dataclasses==0.7;python_version<'3.7'",
|
||||
"wurlitzer>=2.0.1",
|
||||
"photoscript>=0.1.0",
|
||||
"photoscript>=0.1.2",
|
||||
"toml>=0.10.0",
|
||||
"osxmetadata>=0.99.13",
|
||||
"textx==2.3.0",
|
||||
"rich>=9.11.1",
|
||||
"bitmath==1.3.3.1",
|
||||
],
|
||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||
include_package_data=True,
|
||||
|
||||
@@ -1,9 +1,116 @@
|
||||
""" pytest test configuration """
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
from applescript import AppleScript
|
||||
from photoscript.utils import ditto
|
||||
|
||||
from osxphotos.exiftool import _ExifToolProc
|
||||
|
||||
|
||||
def get_os_version():
|
||||
import platform
|
||||
|
||||
# returns tuple containing OS version
|
||||
# e.g. 10.13.6 = (10, 13, 6)
|
||||
version = platform.mac_ver()[0].split(".")
|
||||
if len(version) == 2:
|
||||
(ver, major) = version
|
||||
minor = "0"
|
||||
elif len(version) == 3:
|
||||
(ver, major, minor) = version
|
||||
else:
|
||||
raise (
|
||||
ValueError(
|
||||
f"Could not parse version string: {platform.mac_ver()} {version}"
|
||||
)
|
||||
)
|
||||
return (ver, major, minor)
|
||||
|
||||
|
||||
OS_VER = get_os_version()[1]
|
||||
if OS_VER == "15":
|
||||
TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
|
||||
else:
|
||||
TEST_LIBRARY = None
|
||||
pytest.exit("This test suite currently only runs on MacOS Catalina ")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_singletons():
|
||||
""" Need to clean up any ExifTool singletons between tests """
|
||||
_ExifToolProc.instance = None
|
||||
_ExifToolProc.instance = None
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--addalbum",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="run --add-exported-to-album tests",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "addalbum: mark test as requiring --addalbum to run"
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if config.getoption("--addalbum"):
|
||||
# --addalbum given in cli: do not skip addalbum tests (these require interactive test)
|
||||
return
|
||||
skip_addalbum = pytest.mark.skip(reason="need --addalbum option to run")
|
||||
for item in items:
|
||||
if "addalbum" in item.keywords:
|
||||
item.add_marker(skip_addalbum)
|
||||
|
||||
|
||||
def copy_photos_library(photos_library=TEST_LIBRARY, delay=0):
|
||||
""" copy the test library and open Photos, returns path to copied library """
|
||||
script = AppleScript(
|
||||
"""
|
||||
tell application "Photos"
|
||||
quit
|
||||
end tell
|
||||
"""
|
||||
)
|
||||
script.run()
|
||||
src = pathlib.Path(os.getcwd()) / photos_library
|
||||
picture_folder = (
|
||||
pathlib.Path(os.environ["PHOTOSCRIPT_PICTURES_FOLDER"])
|
||||
if "PHOTOSCRIPT_PICTURES_FOLDER" in os.environ
|
||||
else pathlib.Path("~/Pictures")
|
||||
)
|
||||
picture_folder = picture_folder.expanduser()
|
||||
if not picture_folder.is_dir():
|
||||
pytest.exit(f"Invalid picture folder: '{picture_folder}'")
|
||||
dest = picture_folder / photos_library
|
||||
ditto(src, dest)
|
||||
script = AppleScript(
|
||||
f"""
|
||||
set tries to 0
|
||||
repeat while tries < 5
|
||||
try
|
||||
tell application "Photos"
|
||||
activate
|
||||
delay 3
|
||||
open POSIX file "{dest}"
|
||||
delay {delay}
|
||||
end tell
|
||||
set tries to 5
|
||||
on error
|
||||
set tries to tries + 1
|
||||
end try
|
||||
end repeat
|
||||
"""
|
||||
)
|
||||
script.run()
|
||||
return dest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def addalbum_library():
|
||||
copy_photos_library(delay=10)
|
||||
|
||||
@@ -23,6 +23,14 @@ class PhotoInfoMock(PhotoInfo):
|
||||
else self._photo.hdr
|
||||
)
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
return (
|
||||
self._mock_favorite
|
||||
if getattr(self, "_mock_favorite", None) is not None
|
||||
else self._photo.favorite
|
||||
)
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
return (
|
||||
|
||||
File diff suppressed because one or more lines are too long
17
tests/template_filter.py
Normal file
17
tests/template_filter.py
Normal file
@@ -0,0 +1,17 @@
|
||||
""" Example of using a custom python function as an osxphotos template filter
|
||||
|
||||
Use in formath:
|
||||
"{template_field|template_filter.py::myfilter}"
|
||||
|
||||
Your filter function will receive a list of strings even if the template renders to a single value.
|
||||
You should expect a list and return a list and be able to handle multi-value templates like {keyword}
|
||||
as well as single-value templates like {original_name}
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
def myfilter(values: List[str]) -> List[str]:
|
||||
""" Custom filter to append "foo-" to template value """
|
||||
values = ["foo-" + val for val in values]
|
||||
return values
|
||||
|
||||
20
tests/template_function.py
Normal file
20
tests/template_function.py
Normal file
@@ -0,0 +1,20 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template """
|
||||
|
||||
import pathlib
|
||||
from typing import List, Union
|
||||
|
||||
import osxphotos
|
||||
|
||||
|
||||
def foo(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
||||
""" example function for {function} template
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
return photo.original_filename + "-FOO"
|
||||
@@ -316,6 +316,16 @@ def test_attributes(photosdb):
|
||||
assert p.date == datetime.datetime(
|
||||
2018, 9, 28, 16, 7, 7, 0, datetime.timezone(datetime.timedelta(seconds=-14400))
|
||||
)
|
||||
assert p.date_added == datetime.datetime(
|
||||
2019,
|
||||
7,
|
||||
27,
|
||||
9,
|
||||
16,
|
||||
49,
|
||||
778432,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)),
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert sorted(p.albums) == ["Pumpkin Farm", "Test Album"]
|
||||
@@ -848,7 +858,9 @@ def test_export_12(photosdb):
|
||||
|
||||
edited_name = pathlib.Path(photos[0].path_edited).name
|
||||
edited_suffix = pathlib.Path(edited_name).suffix
|
||||
filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
|
||||
filename = (
|
||||
pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
|
||||
)
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
@@ -1098,4 +1110,4 @@ def test_no_adjustments(photosdb):
|
||||
""" test adjustments when photo has no adjusments"""
|
||||
|
||||
photo = photosdb.get_photo(UUID_DICT["no_adjustments"])
|
||||
assert photo.adjustments is None
|
||||
assert photo.adjustments is None
|
||||
|
||||
@@ -116,6 +116,12 @@ UUID_DICT = {
|
||||
UUID_DICT_LOCAL = {
|
||||
"not_visible": "ABF00253-78E7-4FD6-953B-709307CD489D",
|
||||
"burst": "44AF1FCA-AC2D-4FA5-B288-E67DC18F9CA8",
|
||||
"burst_key": "9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2",
|
||||
"burst_not_key": "38F8F30C-FF6D-49DA-8092-18497F1D6628",
|
||||
"burst_selected": "38F8F30C-FF6D-49DA-8092-18497F1D6628",
|
||||
"burst_not_selected": "A385FA13-DF8E-482F-A8C5-970EDDF54C2F",
|
||||
"burst_default": "964F457D-5FFC-47B9-BEAD-56B0A83FEF63",
|
||||
"burst_not_default": "A385FA13-DF8E-482F-A8C5-970EDDF54C2F",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
@@ -353,6 +359,16 @@ def test_attributes(photosdb):
|
||||
assert p.date == datetime.datetime(
|
||||
2018, 9, 28, 16, 7, 7, 0, datetime.timezone(datetime.timedelta(seconds=-14400))
|
||||
)
|
||||
assert p.date_added == datetime.datetime(
|
||||
2019,
|
||||
7,
|
||||
27,
|
||||
9,
|
||||
16,
|
||||
49,
|
||||
778432,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)),
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert sorted(p.albums) == ["Pumpkin Farm", "Test Album"]
|
||||
@@ -890,7 +906,9 @@ def test_export_12(photosdb):
|
||||
|
||||
edited_name = pathlib.Path(photos[0].path_edited).name
|
||||
edited_suffix = pathlib.Path(edited_name).suffix
|
||||
filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
|
||||
filename = (
|
||||
pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
|
||||
)
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
@@ -1199,6 +1217,36 @@ def test_visible_burst(photosdb_local):
|
||||
assert len(photo.burst_photos) == 4
|
||||
|
||||
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_burst_key(photosdb_local):
|
||||
""" test burst_key """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_key"])
|
||||
assert photo.burst_key
|
||||
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_not_key"])
|
||||
assert not photo.burst_key
|
||||
|
||||
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_burst_selected(photosdb_local):
|
||||
""" test burst_selected """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_selected"])
|
||||
assert photo.burst_selected
|
||||
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_not_selected"])
|
||||
assert not photo.burst_selected
|
||||
|
||||
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_burst_default_pic(photosdb_local):
|
||||
""" test burst_default_pick"""
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_default"])
|
||||
assert photo.burst_default_pick
|
||||
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst_not_default"])
|
||||
assert not photo.burst_default_pick
|
||||
|
||||
|
||||
def test_is_reference(photosdb):
|
||||
""" test isreference """
|
||||
|
||||
|
||||
@@ -22,6 +22,27 @@ PHOTOS_DB_TOUCH = PHOTOS_DB_15_6
|
||||
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
|
||||
PHOTOS_DB_MOVIES = "tests/Test-Movie-5_0.photoslibrary"
|
||||
|
||||
# my personal library which some tests require
|
||||
PHOTOS_DB_RHET = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
UUID_BURST_ALBUM = {
|
||||
"9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2": [
|
||||
"TestBurst/IMG_9812.JPG", # in my personal library, IMG_9812.JPG == "9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2"
|
||||
"TestBurst/IMG_9813.JPG",
|
||||
"TestBurst/IMG_9814.JPG",
|
||||
"TestBurst/IMG_9815.JPG",
|
||||
"TestBurst/IMG_9816.JPG",
|
||||
"TestBurst2/IMG_9814.JPG",
|
||||
],
|
||||
"38F8F30C-FF6D-49DA-8092-18497F1D6628": [
|
||||
"TestBurst/IMG_9812.JPG",
|
||||
"TestBurst/IMG_9813.JPG",
|
||||
"TestBurst/IMG_9814.JPG", # in my personal library, "38F8F30C-FF6D-49DA-8092-18497F1D6628"
|
||||
"TestBurst/IMG_9815.JPG",
|
||||
"TestBurst/IMG_9816.JPG",
|
||||
"TestBurst2/IMG_9814.JPG",
|
||||
],
|
||||
}
|
||||
|
||||
UUID_FILE = "tests/uuid_from_file.txt"
|
||||
|
||||
CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
@@ -451,7 +472,17 @@ CLI_FINDER_TAGS = {
|
||||
"XMP:Description": "Girl holding pumpkin",
|
||||
"XMP:PersonInImage": "Katie",
|
||||
"XMP:Subject": "Kids",
|
||||
}
|
||||
},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
|
||||
"File:FileName": "wedding.jpg",
|
||||
"IPTC:Keywords": ["Maria", "wedding"],
|
||||
"XMP:TagsList": ["Maria", "wedding"],
|
||||
"XMP:Title": None,
|
||||
"EXIF:ImageDescription": "Bride Wedding day",
|
||||
"XMP:Description": "Bride Wedding day",
|
||||
"XMP:PersonInImage": "Maria",
|
||||
"XMP:Subject": ["Maria", "wedding"],
|
||||
},
|
||||
}
|
||||
|
||||
LABELS_JSON = {
|
||||
@@ -5319,7 +5350,7 @@ def test_export_finder_tag_template_multiple():
|
||||
keywords = [keywords] if type(keywords) != list else keywords
|
||||
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
persons = [persons] if type(persons) != list else persons
|
||||
expected = [Tag(x) for x in keywords + persons]
|
||||
expected = [Tag(x) for x in set(keywords + persons)]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@@ -5357,7 +5388,42 @@ def test_export_finder_tag_template_keywords():
|
||||
keywords = [keywords] if type(keywords) != list else keywords
|
||||
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
persons = [persons] if type(persons) != list else persons
|
||||
expected = [Tag(x) for x in keywords + persons]
|
||||
expected = [Tag(x) for x in set(keywords + persons)]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
def test_export_finder_tag_template_multi_field():
|
||||
""" test --finder-tag-template with multiple fields (issue #422) """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from osxmetadata import OSXMetaData, Tag
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
for uuid in CLI_FINDER_TAGS:
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--finder-tag-template",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"])
|
||||
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
|
||||
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
|
||||
expected = [Tag(f"{title};{descr}")]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@@ -5386,7 +5452,7 @@ def test_export_xattr_template():
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
@@ -5397,7 +5463,9 @@ def test_export_xattr_template():
|
||||
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
expected = [expected] if type(expected) != list else expected
|
||||
assert sorted(md.keywords) == sorted(expected)
|
||||
assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"]
|
||||
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
|
||||
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
|
||||
assert md.comment == f"{title};{descr}"
|
||||
|
||||
# run again with --update, should skip writing extended attributes
|
||||
result = runner.invoke(
|
||||
@@ -5411,7 +5479,7 @@ def test_export_xattr_template():
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
"--update",
|
||||
@@ -5649,3 +5717,395 @@ def test_export_jpeg_ext_convert_to_jpeg_movie():
|
||||
assert f"{filename}.jpg".lower() not in files
|
||||
assert f"{filename}.{ext}".lower() in files
|
||||
assert f"{filename}_edited.{ext}".lower() in files
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
||||
reason="Skip if not running on author's personal library.",
|
||||
)
|
||||
def test_export_burst_folder_album():
|
||||
""" test non-selected burst photos are exported with the album their key photo is in, issue #401 """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
for uuid in UUID_BURST_ALBUM:
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_RHET),
|
||||
".",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{folder_album}",
|
||||
"--uuid",
|
||||
uuid,
|
||||
"--download-missing",
|
||||
"--use-photokit",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = [str(p) for p in pathlib.Path(".").glob("**/*.JPG")]
|
||||
assert sorted(files) == sorted(UUID_BURST_ALBUM[uuid])
|
||||
|
||||
|
||||
def test_query_name():
|
||||
""" test query --name """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "DSC03584"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
assert json_got[0]["original_filename"] == "DSC03584.dng"
|
||||
|
||||
|
||||
def test_query_name_i():
|
||||
""" test query --name -i """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--name",
|
||||
"dsc03584",
|
||||
"-i",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
assert json_got[0]["original_filename"] == "DSC03584.dng"
|
||||
|
||||
|
||||
def test_export_name():
|
||||
""" test export --name """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--name", "DSC03584"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == 1
|
||||
|
||||
|
||||
def test_query_eval():
|
||||
""" test export --query-eval """
|
||||
import glob
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--query-eval",
|
||||
"'DSC03584' in photo.original_filename",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == 1
|
||||
|
||||
|
||||
def test_bad_query_eval():
|
||||
""" test export --query-eval with bad input """
|
||||
import glob
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--query-eval",
|
||||
"'DSC03584' in photo.originalfilename",
|
||||
],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "Error: Invalid query-eval CRITERIA" in result.output
|
||||
|
||||
|
||||
def test_query_min_size_1():
|
||||
""" test query --min-size """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--min-size", "10MB"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 2
|
||||
|
||||
|
||||
def test_query_min_size_2():
|
||||
""" test query --min-size """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--min-size",
|
||||
"10_000_000",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 2
|
||||
|
||||
|
||||
def test_query_max_size_1():
|
||||
""" test query --max-size """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--max-size", "500 kB"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
|
||||
|
||||
def test_query_max_size_2():
|
||||
""" test query --max-size """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--max-size", "500_000"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
|
||||
|
||||
def test_query_min_max_size():
|
||||
""" test query --max-size with --min-size"""
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--min-size",
|
||||
"48MB",
|
||||
"--max-size",
|
||||
"49MB",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
|
||||
|
||||
def test_query_min_size_error():
|
||||
""" test query --max-size with invalid size """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--min-size", "500 foo"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
def test_query_regex_1():
|
||||
""" test query --regex against title """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--regex",
|
||||
"I found",
|
||||
"{title}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
|
||||
|
||||
def test_query_regex_2():
|
||||
""" test query --regex with no match"""
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--regex",
|
||||
"{title}",
|
||||
"i Found",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 0
|
||||
|
||||
|
||||
def test_query_regex_3():
|
||||
""" test query --regex with --ignore-case """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--regex",
|
||||
"i Found",
|
||||
"{title}",
|
||||
"--ignore-case",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1
|
||||
|
||||
|
||||
def test_query_regex_4():
|
||||
""" test query --regex against album """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--regex",
|
||||
"^Test",
|
||||
"{album}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 2
|
||||
|
||||
115
tests/test_cli_add_to_album.py
Normal file
115
tests/test_cli_add_to_album.py
Normal file
@@ -0,0 +1,115 @@
|
||||
""" Test --add-exported-to-album """
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from click.testing import CliRunner
|
||||
import photoscript
|
||||
|
||||
UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}}
|
||||
UUID_MISSING = {
|
||||
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.addalbum
|
||||
def test_export_add_to_album(addalbum_library):
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
with runner.isolated_filesystem():
|
||||
EXPORT_ALBUM = "OSXPhotos Export"
|
||||
SKIP_ALBUM = "OSXPhotos Skipped"
|
||||
MISSING_ALBUM = "OSXPhotos Missing"
|
||||
|
||||
uuid_opt = [f"--uuid={uuid}" for uuid in UUID_EXPORT]
|
||||
uuid_opt += [f"--uuid={uuid}" for uuid in UUID_MISSING]
|
||||
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
".",
|
||||
"-V",
|
||||
"--add-exported-to-album",
|
||||
EXPORT_ALBUM,
|
||||
"--add-skipped-to-album",
|
||||
SKIP_ALBUM,
|
||||
*uuid_opt,
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert f"Creating Photos album '{EXPORT_ALBUM}'" in result.output
|
||||
assert f"Creating Photos album '{SKIP_ALBUM}'" in result.output
|
||||
|
||||
photoslib = photoscript.PhotosLibrary()
|
||||
album = photoslib.album(EXPORT_ALBUM)
|
||||
assert album is not None
|
||||
|
||||
assert len(album) == len(UUID_EXPORT)
|
||||
got_uuids = [p.uuid for p in album.photos()]
|
||||
assert sorted(got_uuids) == sorted(list(UUID_EXPORT.keys()))
|
||||
|
||||
skip_album = photoslib.album(SKIP_ALBUM)
|
||||
assert skip_album is not None
|
||||
assert len(skip_album) == 0
|
||||
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
".",
|
||||
"-V",
|
||||
"--add-exported-to-album",
|
||||
EXPORT_ALBUM,
|
||||
"--add-skipped-to-album",
|
||||
SKIP_ALBUM,
|
||||
"--add-missing-to-album",
|
||||
MISSING_ALBUM,
|
||||
"--update",
|
||||
*uuid_opt,
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert f"Creating Photos album '{EXPORT_ALBUM}'" not in result.output
|
||||
assert f"Creating Photos album '{SKIP_ALBUM}'" not in result.output
|
||||
assert f"Creating Photos album '{MISSING_ALBUM}'" in result.output
|
||||
|
||||
photoslib = photoscript.PhotosLibrary()
|
||||
export_album = photoslib.album(EXPORT_ALBUM)
|
||||
assert export_album is not None
|
||||
assert len(export_album) == len(UUID_EXPORT)
|
||||
|
||||
skip_album = photoslib.album(SKIP_ALBUM)
|
||||
assert skip_album is not None
|
||||
assert len(skip_album) == len(UUID_EXPORT)
|
||||
got_uuids = [p.uuid for p in skip_album.photos()]
|
||||
assert sorted(got_uuids) == sorted(list(UUID_EXPORT.keys()))
|
||||
|
||||
missing_album = photoslib.album(MISSING_ALBUM)
|
||||
assert missing_album is not None
|
||||
assert len(missing_album) == len(UUID_MISSING)
|
||||
got_uuids = [p.uuid for p in missing_album.photos()]
|
||||
assert sorted(got_uuids) == sorted(list(UUID_MISSING.keys()))
|
||||
|
||||
|
||||
@pytest.mark.addalbum
|
||||
def test_query_add_to_album(addalbum_library):
|
||||
from osxphotos.cli import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
with runner.isolated_filesystem():
|
||||
QUERY_ALBUM = "OSXPhotos Query"
|
||||
|
||||
uuid_opt = [f"--uuid={uuid}" for uuid in UUID_EXPORT]
|
||||
|
||||
result = runner.invoke(query, ["--add-to-album", QUERY_ALBUM, *uuid_opt])
|
||||
assert result.exit_code == 0
|
||||
|
||||
photoslib = photoscript.PhotosLibrary()
|
||||
album = photoslib.album(QUERY_ALBUM)
|
||||
assert album is not None
|
||||
|
||||
assert len(album) == len(UUID_EXPORT)
|
||||
got_uuids = [p.uuid for p in album.photos()]
|
||||
assert sorted(got_uuids) == sorted(list(UUID_EXPORT.keys()))
|
||||
|
||||
@@ -349,6 +349,15 @@ def test_as_dict():
|
||||
assert exifdata["XMP:TagsList"] == "wedding"
|
||||
|
||||
|
||||
def test_as_dict_normalized():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||
exifdata = exif1.asdict(normalized=True)
|
||||
assert exifdata["xmp:tagslist"] == "wedding"
|
||||
assert "XMP:TagsList" not in exifdata
|
||||
|
||||
|
||||
def test_as_dict_no_tag_groups():
|
||||
import osxphotos.exiftool
|
||||
|
||||
|
||||
328
tests/test_exiftool_caching.py
Normal file
328
tests/test_exiftool_caching.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import pytest
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
|
||||
TEST_FILE_ONE_KEYWORD = "tests/test-images/wedding.jpg"
|
||||
TEST_FILE_BAD_IMAGE = "tests/test-images/badimage.jpeg"
|
||||
TEST_FILE_WARNING = "tests/test-images/exiftool_warning.heic"
|
||||
TEST_FILE_MULTI_KEYWORD = "tests/test-images/Tulips.jpg"
|
||||
TEST_MULTI_KEYWORDS = [
|
||||
"Top Shot",
|
||||
"flowers",
|
||||
"flower",
|
||||
"design",
|
||||
"Stock Photography",
|
||||
"vibrant",
|
||||
"plastic",
|
||||
"Digital Nomad",
|
||||
"close up",
|
||||
"stock photo",
|
||||
"outdoor",
|
||||
"wedding",
|
||||
"Reiseblogger",
|
||||
"fake",
|
||||
"colorful",
|
||||
"Indoor",
|
||||
"display",
|
||||
"photography",
|
||||
]
|
||||
|
||||
PHOTOS_DB = "tests/Test-10.15.4.photoslibrary"
|
||||
EXIF_UUID = {
|
||||
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": {
|
||||
"EXIF:DateTimeOriginal": "2019:07:04 16:24:01",
|
||||
"EXIF:LensModel": "XF18-55mmF2.8-4 R LM OIS",
|
||||
"IPTC:Keywords": [
|
||||
"Digital Nomad",
|
||||
"Indoor",
|
||||
"Reiseblogger",
|
||||
"Stock Photography",
|
||||
"Top Shot",
|
||||
"close up",
|
||||
"colorful",
|
||||
"design",
|
||||
"display",
|
||||
"fake",
|
||||
"flower",
|
||||
"outdoor",
|
||||
"photography",
|
||||
"plastic",
|
||||
"stock photo",
|
||||
"vibrant",
|
||||
],
|
||||
"IPTC:DocumentNotes": "https://flickr.com/e/l7FkSm4f2lQkSV3CG6xlv8Sde5uF3gVu4Hf0Qk11AnU%3D",
|
||||
},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
|
||||
"EXIF:Make": "NIKON CORPORATION",
|
||||
"EXIF:Model": "NIKON D810",
|
||||
"IPTC:DateCreated": "2019:04:15",
|
||||
},
|
||||
}
|
||||
EXIF_UUID_NO_GROUPS = {
|
||||
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": {
|
||||
"DateTimeOriginal": "2019:07:04 16:24:01",
|
||||
"LensModel": "XF18-55mmF2.8-4 R LM OIS",
|
||||
"Keywords": [
|
||||
"Digital Nomad",
|
||||
"Indoor",
|
||||
"Reiseblogger",
|
||||
"Stock Photography",
|
||||
"Top Shot",
|
||||
"close up",
|
||||
"colorful",
|
||||
"design",
|
||||
"display",
|
||||
"fake",
|
||||
"flower",
|
||||
"outdoor",
|
||||
"photography",
|
||||
"plastic",
|
||||
"stock photo",
|
||||
"vibrant",
|
||||
],
|
||||
"DocumentNotes": "https://flickr.com/e/l7FkSm4f2lQkSV3CG6xlv8Sde5uF3gVu4Hf0Qk11AnU%3D",
|
||||
},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
|
||||
"Make": "NIKON CORPORATION",
|
||||
"Model": "NIKON D810",
|
||||
"DateCreated": "2019:04:15",
|
||||
},
|
||||
}
|
||||
EXIF_UUID_NONE = ["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"]
|
||||
|
||||
try:
|
||||
exiftool = get_exiftool_path()
|
||||
except:
|
||||
exiftool = None
|
||||
|
||||
if exiftool is None:
|
||||
pytest.skip("could not find exiftool in path", allow_module_level=True)
|
||||
|
||||
|
||||
def test_version():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert exif.version is not None
|
||||
assert isinstance(exif.version, str)
|
||||
|
||||
|
||||
def test_singleton():
|
||||
""" tests per-file singleton behavior """
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
exif2 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert exif1 is exif2
|
||||
|
||||
exif3 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_MULTI_KEYWORD)
|
||||
assert exif1 is not exif3
|
||||
|
||||
|
||||
def test_read():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert exif.data["File:MIMEType"] == "image/jpeg"
|
||||
assert exif.data["EXIF:ISO"] == 160
|
||||
assert exif.data["IPTC:Keywords"] == "wedding"
|
||||
|
||||
|
||||
def test_setvalue_1():
|
||||
# test setting a tag value
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||
|
||||
exif = osxphotos.exiftool.ExifToolCaching(tempfile)
|
||||
with pytest.raises(NotImplementedError):
|
||||
exif.setvalue("IPTC:Keywords", "test")
|
||||
|
||||
|
||||
def test_setvalue_cache():
|
||||
# test setting a tag value doesn't affect cached value
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||
|
||||
exif = osxphotos.exiftool.ExifTool(tempfile)
|
||||
exif.setvalue("IPTC:Keywords", "test")
|
||||
assert exif.asdict()["IPTC:Keywords"] == "test"
|
||||
|
||||
exifcache = osxphotos.exiftool.ExifToolCaching(tempfile)
|
||||
assert exifcache.asdict()["IPTC:Keywords"] == "test"
|
||||
|
||||
# now change the value
|
||||
exif.setvalue("IPTC:Keywords", "foo")
|
||||
assert exif.asdict()["IPTC:Keywords"] == "foo"
|
||||
assert exifcache.asdict()["IPTC:Keywords"] == "test"
|
||||
|
||||
exifcache.flush_cache()
|
||||
assert exifcache.asdict()["IPTC:Keywords"] == "foo"
|
||||
|
||||
|
||||
def test_setvalue_context_manager():
|
||||
# test setting a tag value as context manager
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
with osxphotos.exiftool.ExifToolCaching(tempfile) as exif:
|
||||
exif.setvalue("IPTC:Keywords", "test1")
|
||||
|
||||
|
||||
def test_flags():
|
||||
# test that flags raise error
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_WARNING))
|
||||
FileUtil.copy(TEST_FILE_WARNING, tempfile)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
# ExifToolCaching doesn't take flags arg
|
||||
with osxphotos.exiftool.ExifToolCaching(tempfile, flags=["-m"]) as exif:
|
||||
exif.setvalue("XMP:Subject", "foo/ba r")
|
||||
|
||||
|
||||
def test_clear_value():
|
||||
# test clearing a tag value
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||
|
||||
exif = osxphotos.exiftool.ExifToolCaching(tempfile)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
exif.setvalue("IPTC:Keywords", None)
|
||||
|
||||
|
||||
def test_addvalues_1():
|
||||
# test setting a tag value
|
||||
import os.path
|
||||
import tempfile
|
||||
import osxphotos.exiftool
|
||||
from osxphotos.fileutil import FileUtil
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||
|
||||
exif = osxphotos.exiftool.ExifToolCaching(tempfile)
|
||||
with pytest.raises(NotImplementedError):
|
||||
exif.addvalues("IPTC:Keywords", "test")
|
||||
|
||||
|
||||
def test_exiftoolproc_process():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert exif1._exiftoolproc.process is not None
|
||||
|
||||
|
||||
def test_exiftoolproc_exiftool():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert exif1._exiftoolproc.exiftool == osxphotos.exiftool.get_exiftool_path()
|
||||
|
||||
|
||||
def test_as_dict():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
exifdata = exif1.asdict()
|
||||
assert exifdata["XMP:TagsList"] == "wedding"
|
||||
|
||||
|
||||
def test_as_dict_normalized():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
exifdata = exif1.asdict(normalized=True)
|
||||
assert exifdata["xmp:tagslist"] == "wedding"
|
||||
assert "XMP:TagsList" not in exifdata
|
||||
|
||||
|
||||
def test_as_dict_no_tag_groups():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
exifdata = exif1.asdict(tag_groups=False)
|
||||
assert exifdata["TagsList"] == "wedding"
|
||||
|
||||
|
||||
def test_json():
|
||||
import osxphotos.exiftool
|
||||
import json
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
exifdata = json.loads(exif1.json())
|
||||
assert exifdata[0]["XMP:TagsList"] == "wedding"
|
||||
|
||||
|
||||
def test_str():
|
||||
import osxphotos.exiftool
|
||||
|
||||
exif1 = osxphotos.exiftool.ExifToolCaching(TEST_FILE_ONE_KEYWORD)
|
||||
assert "file: " in str(exif1)
|
||||
assert "exiftool: " in str(exif1)
|
||||
|
||||
|
||||
def test_photoinfo_exiftool():
|
||||
""" test PhotoInfo.exiftool which returns ExifTool object for photo """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
for uuid in EXIF_UUID:
|
||||
photo = photosdb.photos(uuid=[uuid])[0]
|
||||
exiftool = photo.exiftool
|
||||
exif_dict = exiftool.asdict()
|
||||
for key, val in EXIF_UUID[uuid].items():
|
||||
assert exif_dict[key] == val
|
||||
|
||||
|
||||
def test_photoinfo_exiftool_no_groups():
|
||||
""" test PhotoInfo.exiftool which returns ExifTool object for photo without tag group names"""
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
for uuid in EXIF_UUID_NO_GROUPS:
|
||||
photo = photosdb.photos(uuid=[uuid])[0]
|
||||
exiftool = photo.exiftool
|
||||
exif_dict = exiftool.asdict(tag_groups=False)
|
||||
for key, val in EXIF_UUID_NO_GROUPS[uuid].items():
|
||||
assert exif_dict[key] == val
|
||||
|
||||
|
||||
def test_photoinfo_exiftool_none():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
for uuid in EXIF_UUID_NONE:
|
||||
photo = photosdb.photos(uuid=[uuid])[0]
|
||||
exiftool = photo.exiftool
|
||||
assert exiftool is None
|
||||
@@ -17,6 +17,39 @@ UUID_DICT = {
|
||||
"live": "BFF29EBD-22DF-4FCF-9817-317E7104EA50",
|
||||
}
|
||||
|
||||
UUID_BURSTS = {
|
||||
"9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2": {
|
||||
"selected": False,
|
||||
"filename": "IMG_9812.JPG",
|
||||
"burst_albums": ["TestBurst"],
|
||||
"albums": ["TestBurst"]
|
||||
},
|
||||
"A385FA13-DF8E-482F-A8C5-970EDDF54C2F": {
|
||||
"selected": False,
|
||||
"filename": "IMG_9813.JPG",
|
||||
"burst_albums": ["TestBurst"],
|
||||
"albums": []
|
||||
},
|
||||
"38F8F30C-FF6D-49DA-8092-18497F1D6628": {
|
||||
"selected": True,
|
||||
"filename": "IMG_9814.JPG",
|
||||
"burst_albums": ["TestBurst", "TestBurst2"],
|
||||
"albums": ["TestBurst2"]
|
||||
},
|
||||
"E3863443-9EA8-417F-A90B-8F7086623DAD": {
|
||||
"selected": False,
|
||||
"filename": "IMG_9815.JPG",
|
||||
"burst_albums": ["TestBurst"],
|
||||
"albums": []
|
||||
},
|
||||
"964F457D-5FFC-47B9-BEAD-56B0A83FEF63": {
|
||||
"selected": True,
|
||||
"filename": "IMG_9816.JPG",
|
||||
"burst_albums": ["TestBurst"],
|
||||
"albums": []
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb():
|
||||
@@ -156,3 +189,14 @@ def test_export_edited_no_edit(photosdb):
|
||||
with pytest.raises(Exception) as e:
|
||||
assert photos[0].export(dest, use_photos_export=True, edited=True)
|
||||
assert e.type == ValueError
|
||||
|
||||
|
||||
def test_burst_albums(photosdb):
|
||||
"""Test burst_selected, burst_albums"""
|
||||
|
||||
for uuid in UUID_BURSTS:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
assert photo.burst
|
||||
assert photo.burst_selected == UUID_BURSTS[uuid]["selected"]
|
||||
assert sorted(photo.albums) == sorted(UUID_BURSTS[uuid]["albums"])
|
||||
assert sorted(photo.burst_albums) == sorted(UUID_BURSTS[uuid]["burst_albums"])
|
||||
|
||||
@@ -47,6 +47,9 @@ def test_exportresults_init():
|
||||
assert results.exiftool_error == []
|
||||
assert results.deleted_files == []
|
||||
assert results.deleted_directories == []
|
||||
assert results.exported_album == []
|
||||
assert results.skipped_album == []
|
||||
assert results.missing_album == []
|
||||
|
||||
|
||||
def test_exportresults_iadd():
|
||||
@@ -110,6 +113,6 @@ def test_str():
|
||||
results = ExportResults()
|
||||
assert (
|
||||
str(results)
|
||||
== "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[],deleted_files=[],deleted_directories=[])"
|
||||
== "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[],deleted_files=[],deleted_directories=[],exported_album=[],skipped_album=[],missing_album=[])"
|
||||
)
|
||||
|
||||
|
||||
@@ -244,6 +244,15 @@ def test_attributes(photosdb):
|
||||
assert p.date == datetime.datetime(
|
||||
2018, 9, 28, 16, 7, 7, 0, datetime.timezone(datetime.timedelta(seconds=-14400))
|
||||
)
|
||||
assert p.date_added == datetime.datetime(
|
||||
2019,
|
||||
7,
|
||||
27,
|
||||
9,
|
||||
16,
|
||||
50,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)),
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert sorted(p.albums) == sorted(
|
||||
@@ -651,4 +660,4 @@ def test_no_adjustments(photosdb):
|
||||
""" test adjustments when photo has no adjusments"""
|
||||
|
||||
photo = photosdb.get_photo(UUID_DICT["no_adjustments"])
|
||||
assert photo.adjustments is None
|
||||
assert photo.adjustments is None
|
||||
|
||||
@@ -54,7 +54,7 @@ UUID_DICT = {
|
||||
"burst": {
|
||||
"uuid": "CD97EC84-71F0-40C6-BAC1-2BABEE305CAC",
|
||||
"filename": "IMG_8196.JPG",
|
||||
"burst_selected": 3,
|
||||
"burst_selected": 4,
|
||||
"burst_all": 5,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ UUID_DICT = {
|
||||
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
|
||||
"date_modified": "A9B73E13-A6F2-4915-8D67-7213B39BAE9F",
|
||||
"date_not_modified": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
}
|
||||
|
||||
UUID_MEDIA_TYPE = {
|
||||
@@ -86,11 +87,13 @@ TEMPLATE_VALUES_TITLE = {
|
||||
UUID_BOOL_VALUES = {
|
||||
"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15",
|
||||
"edited": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
|
||||
"edited_version": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
|
||||
}
|
||||
|
||||
# Boolean type values that render to False
|
||||
UUID_BOOL_VALUES_NOT = {
|
||||
"hdr": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
|
||||
"edited_version": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
|
||||
"edited": "CCBE0EB9-AE9F-4479-BFFD-107042C75227",
|
||||
}
|
||||
|
||||
@@ -133,6 +136,9 @@ UUID_EXIFTOOL = {
|
||||
"England,London,London 2018,St. James's Park,UK,United Kingdom"
|
||||
],
|
||||
},
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C": {
|
||||
"{exiftool:EXIF:SubSecTimeOriginal}": ["22"]
|
||||
},
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES = {
|
||||
@@ -173,6 +179,8 @@ TEMPLATE_VALUES = {
|
||||
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
|
||||
"{album?{folder_album},{created.year}/{created.mm}}": "2020/02",
|
||||
"{title?Title is '{title} - {descr}',No Title}": "Title is 'Glen Ord - Jack Rose Dining Saloon'",
|
||||
"{favorite}": "_",
|
||||
"{favorite?FAV,NOTFAV}": "NOTFAV",
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +258,77 @@ COMMENT_UUID_DICT = {
|
||||
"4E4944A0-3E5C-4028-9600-A8709F2FA1DB": ["None: Nice trophy"],
|
||||
}
|
||||
|
||||
UUID_PHOTO = {
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
|
||||
"{photo.title}": ["St. James's Park"],
|
||||
"{photo.favorite?FAVORITE,NOTFAVORITE}": ["NOTFAVORITE"],
|
||||
"{photo.hdr}": ["_"],
|
||||
"{photo.keywords}": [
|
||||
"England",
|
||||
"London",
|
||||
"London 2018",
|
||||
"St. James's Park",
|
||||
"UK",
|
||||
"United Kingdom",
|
||||
],
|
||||
"{photo.keywords|lower}": [
|
||||
"england",
|
||||
"london",
|
||||
"london 2018",
|
||||
"st. james's park",
|
||||
"uk",
|
||||
"united kingdom",
|
||||
],
|
||||
},
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"{photo.place.country_code}": ["AU"]},
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": {"{photo.place.name}": ["_"]},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {"{photo.favorite}": ["favorite"]},
|
||||
}
|
||||
|
||||
UUID_CONDITIONAL = {
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {
|
||||
"{title matches Elder Park?YES,NO}": ["YES"],
|
||||
"{title matches not Elder Park?YES,NO}": ["NO"],
|
||||
"{title contains Park?YES,NO}": ["YES"],
|
||||
"{title not contains Park?YES,NO}": ["NO"],
|
||||
"{title matches Park?YES,NO}": ["NO"],
|
||||
"{title matches Elder Park?YES,NO}": ["YES"],
|
||||
"{title == Elder Park?YES,NO}": ["YES"],
|
||||
"{title != Elder Park?YES,NO}": ["NO"],
|
||||
"{title[ ,] == ElderPark?YES,NO}": ["YES"],
|
||||
"{title not != Elder Park?YES,NO}": ["YES"],
|
||||
"{title not == Elder Park?YES,NO}": ["NO"],
|
||||
"{title endswith Park?YES,NO}": ["YES"],
|
||||
"{title endswith Elder?YES,NO}": ["NO"],
|
||||
"{title startswith Elder?YES,NO}": ["YES"],
|
||||
"{title endswith Elder?YES,NO}": ["NO"],
|
||||
"{photo.place.name contains Adelaide?YES,NO}": ["YES"],
|
||||
"{photo.place.name|lower contains adelaide?YES,NO}": ["YES"],
|
||||
"{photo.place.name|lower not contains adelaide?YES,NO}": ["NO"],
|
||||
"{photo.score.overall < 0.7?YES,NO}": ["YES"],
|
||||
"{photo.score.overall <= 0.7?YES,NO}": ["YES"],
|
||||
"{photo.score.overall > 0.7?YES,NO}": ["NO"],
|
||||
"{photo.score.overall >= 0.7?YES,NO}": ["NO"],
|
||||
"{photo.score.overall not < 0.7?YES,NO}": ["NO"],
|
||||
"{folder_album(-) contains Folder1-SubFolder2-AlbumInFolder?YES,NO}": ["YES"],
|
||||
"{folder_album(-)[In,] contains Folder1-SubFolder2-AlbumFolder?YES,NO}": [
|
||||
"YES"
|
||||
],
|
||||
},
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
|
||||
"{keyword == {keyword}?YES,NO}": ["YES"],
|
||||
"{keyword contains England?YES,NO}": ["YES"],
|
||||
"{keyword contains Eng?YES,NO}": ["YES"],
|
||||
"{keyword contains Foo?YES,NO}": ["NO"],
|
||||
"{keyword matches England?YES,NO}": ["YES"],
|
||||
"{keyword matches Eng?YES,NO}": ["NO"],
|
||||
"{keyword contains Foo|Bar|England?YES,NO}": ["YES"],
|
||||
"{keyword contains Foo|Bar?YES,NO}": ["NO"],
|
||||
"{keyword matches Foo|Bar|England?YES,NO}": ["YES"],
|
||||
"{keyword matches Foo|Bar?YES,NO}": ["NO"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb_places():
|
||||
@@ -304,7 +383,7 @@ def test_lookup_multi(photosdb_places):
|
||||
|
||||
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||
if subst == "{exiftool}":
|
||||
if subst in ["{exiftool}", "{photo}", "{function}"]:
|
||||
continue
|
||||
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
||||
assert isinstance(lookup, list)
|
||||
@@ -753,7 +832,10 @@ def test_bool_values(photosdb_cloud):
|
||||
for field, uuid in UUID_BOOL_VALUES.items():
|
||||
if uuid is not None:
|
||||
photo = photosdb_cloud.get_photo(uuid)
|
||||
rendered, _ = photo.render_template("{" + f"{field}" + "?True,False}")
|
||||
edited = field == "edited_version"
|
||||
rendered, _ = photo.render_template(
|
||||
"{" + f"{field}" + "?True,False}", edited=edited
|
||||
)
|
||||
assert rendered[0] == "True"
|
||||
|
||||
|
||||
@@ -828,6 +910,14 @@ def test_edited(photosdb):
|
||||
assert rendered == ["edited"]
|
||||
|
||||
|
||||
def test_favorite(photosdb):
|
||||
""" Test favorite"""
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
photomock = PhotoInfoMock(photo, favorite=True)
|
||||
rendered, _ = photomock.render_template("{favorite}")
|
||||
assert rendered == ["favorite"]
|
||||
|
||||
|
||||
def test_nested_template_bool(photosdb):
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
template = "{hdr?{edited?HDR_EDITED,HDR_NOT_EDITED},{edited?NOT_HDR_EDITED,NOT_HDR_NOT_EDITED}}"
|
||||
@@ -865,3 +955,63 @@ def test_punctuation(photosdb):
|
||||
rendered, _ = photo.render_template("{" + punc + "}")
|
||||
assert rendered[0] == PUNCTUATION[punc]
|
||||
|
||||
|
||||
def test_photo_template(photosdb):
|
||||
for uuid in UUID_PHOTO:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
for template in UUID_PHOTO[uuid]:
|
||||
rendered, _ = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(UUID_PHOTO[uuid][template])
|
||||
|
||||
|
||||
def test_conditional(photosdb):
|
||||
for uuid in UUID_CONDITIONAL:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
for template in UUID_CONDITIONAL[uuid]:
|
||||
rendered, _ = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
|
||||
|
||||
|
||||
def test_function(photosdb):
|
||||
""" Test {function} """
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
rendered, _ = photo.render_template("{function:tests/template_function.py::foo}")
|
||||
assert rendered == [f"{photo.original_filename}-FOO"]
|
||||
|
||||
|
||||
def test_function_bad(photosdb):
|
||||
""" Test invalid {function} """
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
with pytest.raises(ValueError):
|
||||
rendered, _ = photo.render_template(
|
||||
"{function:tests/template_function.py::foobar}"
|
||||
)
|
||||
|
||||
|
||||
def test_function_filter(photosdb):
|
||||
""" Test {field|function} filter"""
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
|
||||
rendered, _ = photo.render_template(
|
||||
"{photo.original_filename|function:tests/template_filter.py::myfilter}"
|
||||
)
|
||||
assert rendered == [f"foo-{photo.original_filename}"]
|
||||
|
||||
rendered, _ = photo.render_template(
|
||||
"{photo.original_filename|lower|function:tests/template_filter.py::myfilter}"
|
||||
)
|
||||
assert rendered == [f"foo-{photo.original_filename.lower()}"]
|
||||
|
||||
rendered, _ = photo.render_template(
|
||||
"{photo.original_filename|function:tests/template_filter.py::myfilter|lower}"
|
||||
)
|
||||
assert rendered == [f"foo-{photo.original_filename.lower()}"]
|
||||
|
||||
|
||||
def test_function_filter_bad(photosdb):
|
||||
""" Test invalid {field|function} filter"""
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
with pytest.raises(ValueError):
|
||||
rendered, _ = photo.render_template(
|
||||
"{photo.original_filename|function:tests/template_filter.py::foobar}"
|
||||
)
|
||||
|
||||
@@ -10,16 +10,19 @@
|
||||
# Running this script ensures the above sections of the README.md contain
|
||||
# the most current information, updated directly from the code.
|
||||
|
||||
import re
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from osxphotos.cli import help
|
||||
from osxphotos.phototemplate import (
|
||||
FILTER_VALUES,
|
||||
TEMPLATE_SUBSTITUTIONS,
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||
FILTER_VALUES,
|
||||
)
|
||||
|
||||
TEMPLATE_HELP = "osxphotos/phototemplate.md"
|
||||
TUTORIAL_HELP = "docsrc/source/tutorial.md"
|
||||
|
||||
USAGE_START = (
|
||||
"<!-- OSXPHOTOS-EXPORT-USAGE:START - Do not remove or modify this section -->"
|
||||
@@ -41,6 +44,15 @@ TEMPLATE_FILTER_TABLE_START = (
|
||||
)
|
||||
TEMPLATE_FILTER_TABLE_STOP = "<!-- OSXPHOTOS-FILTER-TABLE:END -->"
|
||||
|
||||
TUTORIAL_START = "<!-- OSXPHOTOS-TUTORIAL:START -->"
|
||||
TUTORIAL_STOP = "<!-- OSXPHOTOS-TUTORIAL:END -->"
|
||||
|
||||
TUTORIAL_HEADER_START = "<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->"
|
||||
TUTORIAL_HEADER_STOP = "<!-- OSXPHOTOS-TUTORIAL-HEADER:END -->"
|
||||
|
||||
TEMPLATE_SYSTEM_LINK_START = "<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START -->"
|
||||
TEMPLATE_SYSTEM_LINK_STOP = "<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END -->"
|
||||
|
||||
|
||||
def generate_template_table():
|
||||
""" generate template substitution table for README.md """
|
||||
@@ -156,6 +168,30 @@ def main():
|
||||
postfix="\n",
|
||||
)
|
||||
|
||||
# update the tutorial
|
||||
print("Updating tutorial")
|
||||
with open(TUTORIAL_HELP) as fd:
|
||||
tutorial_help = fd.read()
|
||||
|
||||
# indent all Markdown headers one more level
|
||||
tutorial_help = re.sub(r"^([#]+)", r"\1#", tutorial_help, flags=re.MULTILINE)
|
||||
|
||||
# insert link for Template System
|
||||
tutorial_help = replace_text(
|
||||
tutorial_help,
|
||||
TEMPLATE_SYSTEM_LINK_START,
|
||||
TEMPLATE_SYSTEM_LINK_STOP,
|
||||
"[Template System](#template-system)",
|
||||
)
|
||||
|
||||
# remove Tutorial Header
|
||||
tutorial_help = replace_text(
|
||||
tutorial_help, TUTORIAL_HEADER_START, TUTORIAL_HEADER_STOP, ""
|
||||
)
|
||||
|
||||
# insert tutorial text into readme
|
||||
new_readme = replace_text(new_readme, TUTORIAL_START, TUTORIAL_STOP, tutorial_help)
|
||||
|
||||
print("Writing new README.md")
|
||||
with open("README.md", "w") as file:
|
||||
file.write(new_readme)
|
||||
|
||||
Reference in New Issue
Block a user