Compare commits

...

74 Commits

Author SHA1 Message Date
Rhet Turnbull
24ccf798c2 Updated README.md [skip ci] 2021-05-28 09:05:00 -07:00
Rhet Turnbull
a298772515 Updated tested versions to 11.3 2021-05-28 09:02:37 -07:00
Rhet Turnbull
2d68594b78 Fixes for #454 2021-05-28 08:48:21 -07:00
Rhet Turnbull
b026147c9a Updated README.md [skip ci] 2021-05-23 14:26:01 -07:00
Rhet Turnbull
186a5b77d0 Fixed bug in imageconverter exception handling, closes #440 2021-05-23 14:21:13 -07:00
Rhet Turnbull
518f855a9b PhotoInfo.exiftool now returns ExifToolCaching, closes #450 2021-05-23 14:14:22 -07:00
allcontributors[bot]
0d2067787c docs: add kaduskj as a contributor (#453)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-05-23 12:33:08 -07:00
Rhet Turnbull
0448a42329 Updated CHANGELOG.md [skip ci] 2021-05-23 12:30:10 -07:00
Rhet Turnbull
a724e15dd6 Updated README.md [skip ci] 2021-05-23 12:26:51 -07:00
Rhet Turnbull
be8fe9d059 Bug fix for #452 2021-05-23 12:01:36 -07:00
Rhet Turnbull
bd6656107b Updated CHANGELOG.md [skip ci] 2021-05-23 09:46:29 -07:00
Rhet Turnbull
a54e051d41 Updated README.md 2021-05-23 09:37:25 -07:00
Rhet Turnbull
7cde52bf9b Fixed #451, path_derivatives for Photos version <= 4 2021-05-23 09:34:40 -07:00
Rhet Turnbull
96037508c1 README.md update [skip ci] 2021-05-22 10:31:52 -07:00
Rhet Turnbull
9f2268fb2b Cleanup exiftool processes when exiting, #449 2021-05-19 06:16:52 -07:00
Rhet Turnbull
df167c00eb Added osxphotos related template fields, partial fix for #444 2021-05-15 14:46:35 -07:00
Rhet Turnbull
e8f9cda0c6 Update README.md 2021-05-09 18:21:38 -07:00
Rhet Turnbull
d4a951f547 Updated CHANGELOG.md, [skip ci] 2021-05-09 18:17:23 -07:00
Rhet Turnbull
f24e4a7e3c Updated path_derivatives to return results in sorted order (largest to smallest) 2021-05-09 17:52:24 -07:00
Rhet Turnbull
98b84c17f1 Updated CHANGELOG.md, [skip ci] 2021-05-08 23:18:04 -07:00
Rhet Turnbull
78c411a643 Updated docs 2021-05-08 23:10:47 -07:00
Rhet Turnbull
6bdf15b41e Added path_derivatives for Photos <= 4 2021-05-08 22:36:11 -07:00
Rhet Turnbull
a0fcec2a7a Fixed typo in README 2021-05-08 16:08:34 -07:00
Rhet Turnbull
63834ab8ab Added path_derivatives for Photos 5, issue #50 2021-05-08 15:11:59 -07:00
Rhet Turnbull
b23cfa32bb Updated docs 2021-05-07 23:00:17 -07:00
Rhet Turnbull
0e22ce54ab Added date_added for Photos 4, #439 2021-05-07 22:56:03 -07:00
Rhet Turnbull
0f41588701 Added date_added, #439 2021-05-05 06:50:41 -07:00
Rhet Turnbull
442b542794 Added --add-to-album example to README 2021-05-02 13:51:48 -07:00
Rhet Turnbull
88fae81b19 Updated CHANGELOG.md, [skip ci] 2021-05-02 13:51:26 -07:00
Rhet Turnbull
c4fec00f67 Updated docs [skip ci] 2021-05-02 09:15:13 -07:00
Rhet Turnbull
9a0cc3e8fa Added --add-to-album to query 2021-05-02 08:35:37 -07:00
Rhet Turnbull
3ed2362fe3 Updated CHANGELOG.md, [skip ci] 2021-05-01 21:26:49 -07:00
Rhet Turnbull
cd8dd552a4 Add --add-exported-to-album, # 428 2021-05-01 21:15:31 -07:00
Rhet Turnbull
64379f313e Update README.md 2021-04-29 00:53:04 -07:00
Rhet Turnbull
dc53480126 Updated known bugs section 2021-04-29 00:52:20 -07:00
Rhet Turnbull
3e06e0e344 Updated tutorial 2021-04-26 11:30:56 -07:00
Rhet Turnbull
aa9f6520d4 Updated tutorial 2021-04-26 11:30:31 -07:00
Rhet Turnbull
3a56e05c85 Updated tutorial 2021-04-26 11:27:12 -07:00
Rhet Turnbull
454a813908 Updated tutorial 2021-04-26 11:26:35 -07:00
Rhet Turnbull
31e162ba94 Add @pdewost as a contributor 2021-04-26 11:25:00 -07:00
Rhet Turnbull
5b8d51da38 Updated CHANGELOG.md, [skip ci] 2021-04-25 19:09:35 -07:00
Rhet Turnbull
846ea89012 Added PyCharm .idea/ 2021-04-25 18:53:31 -07:00
Rhet Turnbull
dc0bbd5fd6 Updated docs 2021-04-25 18:49:51 -07:00
Rhet Turnbull
91804d53ea Added read-only ExifToolCaching class, to implement #325 2021-04-25 18:44:48 -07:00
Rhet Turnbull
3d26206d91 Added normalized flag to ExifTool.asdict() 2021-04-25 08:31:08 -07:00
Rhet Turnbull
92d9dfaef2 Updated CHANGELOG.md, [skip ci] 2021-04-24 22:49:41 -07:00
Rhet Turnbull
51025e7f8b Added {edited_version} template field, closes #420 2021-04-24 22:20:37 -07:00
Rhet Turnbull
d52e5e9316 Updated CHANGELOG.md, [skip ci] 2021-04-24 20:01:40 -07:00
Rhet Turnbull
3711b3f7f1 Fixed handling of burst image selected/key/default, closes #401 (again) 2021-04-24 19:43:35 -07:00
Rhet Turnbull
48c229b52c Refactored export_photo to enable work on #420 2021-04-24 10:00:42 -07:00
Rhet Turnbull
aad435da36 Updated tutorial 2021-04-23 15:50:58 -07:00
Rhet Turnbull
9c60259089 Added instructions for using pre-built executable 2021-04-23 15:48:12 -07:00
Rhet Turnbull
131105d82c Fixed typo in tutorial 2021-04-23 15:36:56 -07:00
Rhet Turnbull
f54205ff49 Added tutorial to README 2021-04-23 15:29:49 -07:00
Rhet Turnbull
1d14fc8041 Refactored README.md to improve Template System section 2021-04-22 21:42:38 -07:00
Rhet Turnbull
4aec01ad1d Updated CHANGELOG.md, [skip ci] 2021-04-20 21:01:05 -07:00
dependabot[bot]
3630643a0e Bump py from 1.8.0 to 1.10.0 (#434)
Bumps [py](https://github.com/pytest-dev/py) from 1.8.0 to 1.10.0.
- [Release notes](https://github.com/pytest-dev/py/releases)
- [Changelog](https://github.com/pytest-dev/py/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/py/compare/1.8.0...1.10.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 20:34:25 -07:00
Rhet Turnbull
44966c6736 Added --regex query option, closes #433 2021-04-20 20:17:44 -07:00
Rhet Turnbull
7c4b28a35c Updated CHANGELOG.md, [skip ci] 2021-04-18 18:31:41 -07:00
Rhet Turnbull
1cdf4addad Fixed docs for function: filter 2021-04-18 18:05:26 -07:00
Rhet Turnbull
50fa851f23 Updated docs 2021-04-18 17:59:22 -07:00
Rhet Turnbull
a483b8a900 Version bump 2021-04-18 17:53:54 -07:00
Rhet Turnbull
dd6d519135 Added function filter to template system, closes #429 2021-04-18 17:52:14 -07:00
Rhet Turnbull
9371db094e Added template_filter.py to examples 2021-04-18 15:50:41 -07:00
Rhet Turnbull
d9a82f29c7 Updated README.md dependencies [skip ci] 2021-04-18 13:52:00 -07:00
Rhet Turnbull
3f57514fa3 Updated docs [skip ci] 2021-04-18 13:43:02 -07:00
Rhet Turnbull
a5a155bd05 Updated CHANGELOG.md, [skip ci] 2021-04-18 13:42:34 -07:00
Rhet Turnbull
c8ea0b0452 Added re to photosdb for use with query_eval 2021-04-18 13:24:02 -07:00
Rhet Turnbull
81fd51c793 Cleaned up queryoptions.py 2021-04-18 09:07:24 -07:00
Rhet Turnbull
648d399524 Updated CHANGELOG.md, [skip ci] 2021-04-18 09:05:28 -07:00
Rhet Turnbull
345c052353 Refactored _query to PhotosDB.query() 2021-04-18 08:32:13 -07:00
Rhet Turnbull
952f1a6c3c Fixed setup.py 2021-04-17 17:58:15 -07:00
Rhet Turnbull
7ae5b8aae7 Added --min-size, --max-size query options, #425 2021-04-17 17:56:48 -07:00
Rhet Turnbull
2e189d771e Updated docs, added build.sh 2021-04-17 10:00:34 -07:00
61 changed files with 5389 additions and 1468 deletions

View File

@@ -202,6 +202,26 @@
"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"
]
},
{
"login": "kaduskj",
"name": "kaduskj",
"avatar_url": "https://avatars.githubusercontent.com/u/983067?v=4",
"profile": "https://github.com/kaduskj",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ __pycache__
t.out
.vscode/
.tox/
.idea/
dist/
build/
working/

File diff suppressed because it is too large Load Diff

1013
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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

View File

@@ -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: d8381561e4b55688141d3bad9c7d6dc7
config: 210ecd9d654dea5d4c21627449ca1d63
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.42.2 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.42.20 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>

View File

@@ -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 &#8212; osxphotos 0.41.10 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; 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>
@@ -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">&quot;&quot;&quot; return all filenames contained in results &quot;&quot;&quot;</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">&quot;,exiftool_error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_files=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_directories=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,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">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,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">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,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">&quot;</span>
<span class="o">+</span> <span class="s2">&quot;)&quot;</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,7 +1704,7 @@
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;QuickTime:ModifyDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime_tz_to_utc</span><span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span>
<span class="c1"># remove any new lines in any fields</span>
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.6 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.20 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>
@@ -488,16 +492,14 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span>
<span class="sd">&quot;&quot;&quot;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 &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_albums</span> <span class="o">=</span> <span class="p">[]</span>
<span class="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="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="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>
@@ -515,16 +517,14 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</span>
<span class="sd">&quot;&quot;&quot; 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. &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[]</span>
<span class="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="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="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>
@@ -637,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">&quot;&quot;&quot; Date photo was added to the database &quot;&quot;&quot;</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">&quot;added_date&quot;</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">&quot;imageTimeZoneOffsetSeconds&quot;</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">&quot;&quot;&quot; returns (latitude, longitude) as float in degrees or None &quot;&quot;&quot;</span>
@@ -746,7 +763,17 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_selected</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst_key&quot;</span><span class="p">]</span>
<span class="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">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</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">&quot;&quot;&quot; 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 &quot;&quot;&quot;</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">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</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">&quot;&quot;&quot; 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 &quot;&quot;&quot;</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">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</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>
@@ -824,6 +851,60 @@
<span class="k">return</span> <span class="n">photopath</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">path_derivatives</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first) &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_derivatives_4</span><span class="p">()</span>
<span class="n">directory</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># first char of uuid</span>
<span class="n">derivative_path</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">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
<span class="o">/</span> <span class="s2">&quot;resources&quot;</span>
<span class="o">/</span> <span class="s2">&quot;derivatives&quot;</span>
<span class="o">/</span> <span class="n">directory</span>
<span class="p">)</span>
<span class="n">files</span> <span class="o">=</span> <span class="n">derivative_path</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">*.*&quot;</span><span class="p">)</span>
<span class="n">files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span><span class="p">)</span>
<span class="c1"># return list of filename but skip .THM files (these are actually low-res thumbnails in JPEG format but with .THM extension)</span>
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">files</span> <span class="k">if</span> <span class="n">filename</span><span class="o">.</span><span class="n">suffix</span> <span class="o">!=</span> <span class="s2">&quot;.THM&quot;</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">_path_derivatives_4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Return paths to all derivative (preview) files for Photos &lt;= 4&quot;&quot;&quot;</span>
<span class="n">modelid</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">&quot;masterModelID&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="n">modelid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">modelid</span><span class="p">)</span>
<span class="n">derivatives_root</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">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
<span class="o">/</span> <span class="s2">&quot;resources&quot;</span>
<span class="o">/</span> <span class="s2">&quot;proxies&quot;</span>
<span class="o">/</span> <span class="s2">&quot;derivatives&quot;</span>
<span class="o">/</span> <span class="n">folder_id</span>
<span class="p">)</span>
<span class="c1"># photos appears to usually be in &quot;00&quot; subfolder but</span>
<span class="c1"># could be elsewhere--I haven&#39;t figured out this logic yet</span>
<span class="c1"># first see if it&#39;s in 00</span>
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="s2">&quot;00&quot;</span> <span class="o">/</span> <span class="n">file_id</span>
<span class="k">if</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">is_dir</span><span class="p">():</span>
<span class="n">files</span> <span class="o">=</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&quot;*&quot;</span><span class="p">)</span>
<span class="n">files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">files</span><span class="p">]</span>
<span class="c1"># didn&#39;t find derivatives path</span>
<span class="k">for</span> <span class="n">subdir</span> <span class="ow">in</span> <span class="n">derivatives_root</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&quot;*&quot;</span><span class="p">):</span>
<span class="k">if</span> <span class="n">subdir</span><span class="o">.</span><span class="n">is_dir</span><span class="p">():</span>
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="n">subdir</span> <span class="o">/</span> <span class="n">file_id</span>
<span class="k">if</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">is_dir</span><span class="p">():</span>
<span class="n">files</span> <span class="o">=</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&quot;*&quot;</span><span class="p">)</span>
<span class="n">files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">files</span><span class="p">]</span>
<span class="c1"># didn&#39;t find a derivatives path</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">panorama</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if photo is a panorama, otherwise False &quot;&quot;&quot;</span>
@@ -960,6 +1041,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">&quot;&quot;&quot;Renders a template string for PhotoInfo instance using PhotoTemplate</span>
@@ -975,6 +1057,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>
@@ -989,6 +1072,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>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.41.10 documentation</title>
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.42.19 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&quot;&quot;&quot;</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&quot;&quot;&quot;</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">&quot;burst_key&quot;</span>
<span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># it&#39;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">&quot;burst_key&quot;</span>
<span class="p">]</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># it&#39;s a burst photo but not one that&#39;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">&quot;burst&quot;</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">&quot;burst_key&quot;</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">&quot;import_session&quot;</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">&quot;import_uuid&quot;</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">&quot;import_uuid&quot;</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">&quot;fok_import_session&quot;</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">&quot;SELECT modelID, defaultName, type, area &quot;</span> <span class="s2">&quot;FROM RKPlace &quot;</span>
<span class="s2">&quot;SELECT modelID, defaultName, type, area FROM RKPlace&quot;</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">&quot;SELECT modelID, uuid, name, importDate from RKImportGroup&quot;</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">&quot;placeNames&quot;</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">&quot;reverse_geolocation&quot;</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">&quot;import_uuid&quot;</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">&quot;added_date&quot;</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">&quot;added_date&quot;</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">&quot;title&quot;</span><span class="p">]</span>
@@ -1902,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 &quot;&quot;&quot;</span>
@@ -1950,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>
@@ -2038,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">&quot;burst_key&quot;</span>
<span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># it&#39;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">&quot;burst_key&quot;</span>
<span class="p">]</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># it&#39;s a burst photo but not one that&#39;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">&quot;burst&quot;</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">&quot;burst_key&quot;</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>
@@ -2138,6 +2149,11 @@
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;saved_asset_type&quot;</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">&quot;isreference&quot;</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">&quot;added_date&quot;</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">&quot;added_date&quot;</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">&quot;import_session&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
@@ -2815,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">&quot;burst&quot;</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">&quot;burst_key&quot;</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">&quot;burst&quot;</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">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</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">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</span> <span class="n">BURST_KEY</span>
<span class="p">):</span>
<span class="c1"># not a key/selected burst photo, don&#39;t include in returned results</span>
<span class="k">continue</span>
@@ -2866,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">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="n">PhotoInfo</span><span class="p">]:</span>
<span class="sd">&quot;&quot;&quot;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"> &quot;&quot;&quot;</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">&quot;albums&quot;</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">&quot;keywords&quot;</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">&quot;persons&quot;</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">&quot;labels&quot;</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">&gt;=</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">&lt;=</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&#39;t include missing burst images (these can&#39;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&#39;s selected would</span>
<span class="c1"># result in the entire set being added above</span>
<span class="c1"># can&#39;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">&gt;=</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">&lt;=</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">&quot;&quot;</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">&quot;[photo for photo in photos if </span><span class="si">{</span><span class="n">q</span><span class="si">}</span><span class="s2">]&quot;</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">&quot;Invalid query_eval CRITERIA: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</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">&quot;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=&#39;</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">&#39;)&quot;</span>
@@ -2881,6 +3253,34 @@
<span class="sd"> Includes recently deleted photos and non-selected burst images</span>
<span class="sd"> &quot;&quot;&quot;</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">&quot;&quot;&quot;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"> &quot;&quot;&quot;</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>

View 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 iPhones 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 Photos 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 files 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.

View File

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

View File

@@ -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) &#8212; osxphotos 0.42.2 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.20 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>
@@ -495,6 +495,24 @@ 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">&lt;SIZE&gt;</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 &gt;= SIZE bytes. The size evaluated is the photos 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">&lt;SIZE&gt;</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 &lt;= SIZE bytes. The size evaluated is the photos 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">&lt;REGEX</span> <span class="pre">TEMPLATE&gt;</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">&lt;CRITERIA&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-query-eval" title="Permalink to this definition"></a></dt>
@@ -795,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 youre 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">&lt;ALBUM&gt;</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 doesnt 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 dont 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">&lt;ALBUM&gt;</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 doesnt 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 dont 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">&lt;ALBUM&gt;</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 doesnt 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 dont 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">&lt;EXPORTDB_FILE&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-exportdb" title="Permalink to this definition"></a></dt>
@@ -1347,6 +1383,24 @@ 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">&lt;SIZE&gt;</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 &gt;= SIZE bytes. The size evaluated is the photos 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">&lt;SIZE&gt;</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 &lt;= SIZE bytes. The size evaluated is the photos 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">&lt;REGEX</span> <span class="pre">TEMPLATE&gt;</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">&lt;CRITERIA&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-query-eval" title="Permalink to this definition"></a></dt>
@@ -1401,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">&lt;ALBUM&gt;</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 doesnt 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 dont 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">

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.42.2 documentation</title>
<title>Index &#8212; osxphotos 0.42.20 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 &lt;ALBUM&gt;
<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 &lt;ALBUM&gt;
<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 &lt;ALBUM&gt;
<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 &lt;ALBUM&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-query-add-to-album">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--album &lt;ALBUM&gt;
<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 &lt;SIZE&gt;
<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 &lt;SIZE&gt;
<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>
@@ -538,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
@@ -556,8 +605,6 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
--no-place
@@ -803,6 +850,15 @@
<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 &lt;REGEX TEMPLATE&gt;
<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>
@@ -1118,11 +1174,15 @@
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
</li>
</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>
@@ -1157,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>
@@ -1458,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 &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-add-missing-to-album">--add-missing-to-album &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-add-skipped-to-album">--add-skipped-to-album &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-album">--album &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-album-keyword">--album-keyword</a>
@@ -1555,6 +1623,10 @@
<li><a href="cli.html#cmdoption-osxphotos-export-live">--live</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config &lt;config file path&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-max-size">--max-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-min-size">--min-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-missing">--missing</a>
</li>
@@ -1617,6 +1689,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">--query-eval &lt;CRITERIA&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-regex">--regex &lt;REGEX TEMPLATE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
</li>
@@ -1760,6 +1834,8 @@
osxphotos-query command line option
<ul>
<li><a href="cli.html#cmdoption-osxphotos-query-add-to-album">--add-to-album &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-album">--album &lt;ALBUM&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-burst">--burst</a>
@@ -1811,6 +1887,10 @@
<li><a href="cli.html#cmdoption-osxphotos-query-label">--label &lt;LABEL&gt;</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 &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-min-size">--min-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-missing">--missing</a>
</li>
@@ -1871,6 +1951,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">--portrait</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">--query-eval &lt;CRITERIA&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-regex">--regex &lt;REGEX TEMPLATE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a>
</li>
@@ -1910,6 +1992,8 @@
<li><a href="reference.html#osxphotos.PhotoInfo.panorama">panorama() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path">path() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_derivatives">path_derivatives() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_edited">path_edited() (osxphotos.PhotoInfo property)</a>
</li>
@@ -1999,6 +2083,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>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.2 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.20 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">&quot;{place.name.country,NoCountry}/{created.year}&quot;</span>&#160; <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{created.year}&quot;</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">&quot;Big</span> <span class="pre">Videos&quot;</span></code></p>
</div>
</div>
</div>
<div class="section" id="example-uses-of-the-package">

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.42.2 documentation</title>
<title>osxphotos &#8212; osxphotos 0.42.20 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>

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.42.2 documentation</title>
<title>Search &#8212; osxphotos 0.42.20 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
View 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 &#8212; osxphotos 0.42.20 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 thats 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 videossee note above), it probably doesnt 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 isnt 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">&quot;{created.year}/{created.month}&quot;</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 its 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 photos creation date and <code class="docutils literal notranslate"><span class="pre">{created.month}</span></code> is the full month name in the users 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">&quot;{created.year}/{place.name.country}&quot;</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">&quot;{created.year}/{place.name.country,No-Country}&quot;</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 dont 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">&quot;{keyword}&quot;</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">&quot;{title}&quot;</span></code></p>
<p>The above command will export photos using the title. Note that you dont 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 &quot;{title,{original_name}}&quot;
│ ││ │
│ ││ │
Use photo&#39;s title as the filename &lt;──────┘ ││ │
││ │
Value after comma will be used &lt;───────┘│ │
if title is blank │ │
│ │
The default value can be &lt;────┘ │
another template field │
Use photo&#39;s original name if no title &lt;──────┘
</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 thats a favorite:</p>
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename &quot;{original_name}{favorite?#,}&quot;
│ │ │││
│ │ │││
Use photo&#39;s original name as filename &lt;──┘ │ │││
│ │││
&#39;favorite&#39; is True if photo is a Favorite, &lt;───────┘ │││
otherwise, False │││
│││
&#39;?&#39; specifies a conditional &lt;─────────────┘││
││
Value immediately following ? will be used if &lt;──────┘│
preceding template field is True or non-blank │
Value immediately following comma will be used if &lt;──────┘
template field is False or blank (null); in this case
no value is specified so a blank string &quot;&quot; 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">&quot;{keyword}-{original_name}&quot;</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">&quot;_EDIT&quot;</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">&quot;_{modified.year}-{modified.mm}-{modified.dd}&quot;</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 dont specify which library to use, osxphotos will try find the last opened library. Occasionally it cant 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 hasnt 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. Theres 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> &lt;<a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>&gt;`_ 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 &quot;{folder_album(&gt;)}&quot;
│ │
│ │
folder_album results in the folder(s) &lt;──┘ │
and album a photo is contained in │
The value in () is used as the path separator &lt;───────┘
for joining the folders and albums. For example,
if photo is in Folder1/Folder2/Album, (&gt;) produces
&quot;Folder1&gt;Folder2&gt;Album&quot; 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&gt;Folder2&gt;Album”. If you did not include the <code class="docutils literal notranslate"><span class="pre">(&gt;)</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 images 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">&quot;{label}&quot;</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 theres 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 doesnt 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">&quot;{label,nolabel}&quot;</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 wont 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 its never been run before. If the database isnt 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 files 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 themread 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">&quot;Travel&quot;</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">&quot;Travel&quot;</span> <span class="pre">--keyword</span> <span class="pre">&quot;Vacation&quot;</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">&quot;Summer</span> <span class="pre">Vacation&quot;</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">&quot;2020-01-01&quot;</span> <span class="pre">--to-date</span> <span class="pre">&quot;2020-02-28&quot;</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">&quot;{label}&quot;</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 photos 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">&quot;{title}{newline}{descr}&quot;</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, youll 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">&quot;{title}{title?{descr?{newline},},}{descr}&quot;</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}
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──&gt; insert title │ │ │ │ │
│ │ │ │ │ │
└───&gt; is there a title?
│ │ │ │ │
└───&gt; if so, is there a description?
│ │ │ │
└───&gt; if so, insert new line
│ │ │
└───&gt; if descr is blank, insert nothing
│ │
└───&gt; if title is blank, insert nothing
└───&gt; 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">&lt;all</span> <span class="pre">your</span> <span class="pre">options</span> <span class="pre">here&gt;</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>Heres 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 iPhones 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 &quot;videos&quot; 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 &quot;Quik&quot; 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 Photos 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 files 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">&quot;{title}{title?{descr?{newline},},}{descr}&quot;</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">
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/tutorial.md.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>

354
docsrc/source/tutorial.md Normal file
View 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 iPhones 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 Photos 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 files 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.

View 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

View File

@@ -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

View File

@@ -70,6 +70,7 @@ _TESTED_OS_VERSIONS = [
("11", "0"),
("11", "1"),
("11", "2"),
("11", "3"),
]
# Photos 5 has persons who are empty string if unidentified face
@@ -209,4 +210,16 @@ 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

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.3"
__version__ = "0.42.26"

File diff suppressed because it is too large Load Diff

View File

@@ -6,18 +6,30 @@
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
import atexit
import json
import logging
import os
import re
import shutil
import subprocess
from abc import ABC, abstractmethod
from functools import lru_cache # pylint: disable=syntax-error
# exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}"
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
# list of exiftool processes to cleanup when exiting or when terminate is called
EXIFTOOL_PROCESSES = []
@atexit.register
def terminate_exiftool():
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool """
for proc in EXIFTOOL_PROCESSES:
proc._stop_proc()
@lru_cache(maxsize=1)
def get_exiftool_path():
@@ -66,7 +78,8 @@ class _ExifToolProc:
if self._process_running:
return self._process
else:
raise ValueError("exiftool process is not running")
self._start_proc()
return self._process
@property
def pid(self):
@@ -104,30 +117,30 @@ class _ExifToolProc:
)
self._process_running = True
EXIFTOOL_PROCESSES.append(self)
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
if not self._process_running:
return
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
try:
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
except Exception as e:
pass
try:
self._process.communicate(timeout=5)
except subprocess.TimeoutExpired:
logging.warning(
f"exiftool pid {self._process.pid} did not exit, killing it"
)
self._process.kill()
self._process.communicate()
del self._process
self._process_running = False
def __del__(self):
self._stop_proc()
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
@@ -153,9 +166,12 @@ class ExifTool:
# if running as a context manager, self._context_mgr will be True
self._context_mgr = False
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
self._process = self._exiftoolproc.process
self._read_exif()
@property
def _process(self):
return self._exiftoolproc.process
def setvalue(self, tag, value):
"""Set tag to value(s); if value is None, will delete tag
@@ -185,7 +201,7 @@ class ExifTool:
return True
else:
_, _, error = self.run_commands(*command)
return error is None
return error == ""
def addvalues(self, tag, *values):
"""Add one or more value(s) to tag
@@ -227,7 +243,7 @@ class ExifTool:
return True
else:
_, _, error = self.run_commands(*command)
return error is None
return error == ""
def run_commands(self, *commands, no_file=False):
"""Run commands in the exiftool process and return result.
@@ -308,12 +324,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 +351,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 +381,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 = {}

View File

@@ -121,6 +121,6 @@ class ImageConverter:
return True
else:
raise ImageConversionError(
"Error converting file {input_path} to jpeg at {output_path}: {error}"
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
)

View File

@@ -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

View File

@@ -3,13 +3,13 @@
import logging
import os
from ..exiftool import ExifTool, get_exiftool_path
from ..exiftool import ExifToolCaching, get_exiftool_path
@property
def exiftool(self):
""" Returns an ExifTool object for the photo
requires that exiftool (https://exiftool.org/) be installed
""" Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
Requires that exiftool (https://exiftool.org/) be installed
If exiftool not installed, logs warning and returns None
If photo path is missing, returns None
"""
@@ -20,7 +20,7 @@ def exiftool(self):
try:
exiftool_path = self._db._exiftool_path or get_exiftool_path()
if self.path is not None and os.path.isfile(self.path):
exiftool = ExifTool(self.path, exiftool=exiftool_path)
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
else:
exiftool = None
except FileNotFoundError:

View File

@@ -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,7 +1671,7 @@ 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:

View File

@@ -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
@@ -455,16 +459,14 @@ class PhotoInfo:
@property
def burst_albums(self):
"""If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums"""
if self.burst_selected or not self.burst:
return self.albums
"""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 = []
burst_albums = list(self.albums)
for photo in self.burst_photos:
burst_albums.extend(photo.albums)
if photo.burst_key:
burst_albums.extend(photo.albums)
self._burst_albums = list(set(burst_albums))
return self._burst_albums
@@ -482,16 +484,14 @@ class PhotoInfo:
@property
def burst_album_info(self):
""" If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info """
if self.burst_selected or not self.burst:
return self.album_info
""" 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 = []
burst_album_info = list(self.album_info)
for photo in self.burst_photos:
burst_album_info.extend(photo.album_info)
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
@@ -604,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 """
@@ -713,7 +730,17 @@ class PhotoInfo:
@property
def burst_selected(self):
""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """
return self._info["burst_key"]
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):
@@ -791,6 +818,60 @@ class PhotoInfo:
return photopath
@property
def path_derivatives(self):
""" Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first) """
if self._db._db_version <= _PHOTOS_4_VERSION:
return self._path_derivatives_4()
directory = self._uuid[0] # first char of uuid
derivative_path = (
pathlib.Path(self._db._library_path)
/ "resources"
/ "derivatives"
/ directory
)
files = derivative_path.glob(f"{self.uuid}*.*")
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
# return list of filename but skip .THM files (these are actually low-res thumbnails in JPEG format but with .THM extension)
return [str(filename) for filename in files if filename.suffix != ".THM"]
def _path_derivatives_4(self):
""" Return paths to all derivative (preview) files for Photos <= 4"""
modelid = self._info["modelID"]
if modelid is None:
return []
folder_id, file_id = _get_resource_loc(modelid)
derivatives_root = (
pathlib.Path(self._db._library_path)
/ "resources"
/ "proxies"
/ "derivatives"
/ folder_id
)
# photos appears to usually be in "00" subfolder but
# could be elsewhere--I haven't figured out this logic yet
# first see if it's in 00
derivatives_path = derivatives_root / "00" / file_id
if derivatives_path.is_dir():
files = derivatives_path.glob("*")
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
return [str(filename) for filename in files]
# didn't find derivatives path
for subdir in derivatives_root.glob("*"):
if subdir.is_dir():
derivatives_path = derivatives_root / subdir / file_id
if derivatives_path.is_dir():
files = derivatives_path.glob("*")
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
return [str(filename) for filename in files]
# didn't find a derivatives path
return []
@property
def panorama(self):
""" Returns True if photo is a panorama, otherwise False """
@@ -927,6 +1008,7 @@ class PhotoInfo:
filename=False,
dirname=False,
strip=False,
edited=False,
):
"""Renders a template string for PhotoInfo instance using PhotoTemplate
@@ -942,6 +1024,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
@@ -956,6 +1039,7 @@ class PhotoInfo:
filename=filename,
dirname=dirname,
strip=strip,
edited_version=edited,
)
@property

View File

@@ -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
View 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()

View File

@@ -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"]
@@ -1597,7 +1615,11 @@ class PhotosDB:
for person in c:
pk = person[0]
fullname = person[2] if person[2] != "" else _UNKNOWN_PERSON
fullname = (
person[2]
if (person[2] != "" and person[2] is not None)
else _UNKNOWN_PERSON
)
self._dbpersons_pk[pk] = {
"pk": pk,
"uuid": person[1],
@@ -1869,7 +1891,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 """
@@ -1917,6 +1940,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]
@@ -2005,18 +2029,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
@@ -2105,6 +2120,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
@@ -2782,7 +2802,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
@@ -2833,6 +2856,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}')"
@@ -2848,3 +3224,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

View File

@@ -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"]`:

View File

@@ -4,12 +4,14 @@ import datetime
import locale
import os
import pathlib
import sys
from textx import TextXSyntaxError, metamodel_from_file
from ._constants import _UNKNOWN_PERSON
from ._version import __version__
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
@@ -48,7 +50,8 @@ 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",
@@ -133,6 +136,8 @@ TEMPLATE_SUBSTITUTIONS = {
"{lf}": r"A line feed: '\n', alias for {newline}",
"{cr}": r"A carriage return: '\r'",
"{crlf}": r"a carriage return + line feed: '\r\n'",
"{osxphotos_version}": f"The osxphotos version, e.g. '{__version__}'",
"{osxphotos_cmd_line}": "The full command line used to run osxphotos"
}
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -174,6 +179,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
@@ -256,6 +262,9 @@ class PhotoTemplate:
# get parser singleton
self.parser = PhotoTemplateParser()
# should {edited_version} render True?
self.edited_version = False
def render(
self,
template,
@@ -266,6 +275,7 @@ class PhotoTemplate:
filename=False,
dirname=False,
strip=False,
edited_version=False,
):
""" Render a filename or directory template
@@ -280,6 +290,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
@@ -303,6 +314,8 @@ class PhotoTemplate:
# empty string
return [], []
self.edited_version = edited_version
return self._render_statement(
model,
none_str=none_str,
@@ -668,6 +681,8 @@ 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":
@@ -897,6 +912,10 @@ class PhotoTemplate:
value = self.photo.uuid
elif field in PUNCTUATION:
value = PUNCTUATION[field]
elif field == "osxphotos_version":
value = __version__
elif field == "osxphotos_cmd_line":
value = " ".join(sys.argv)
else:
# if here, didn't get a match
raise ValueError(f"Unhandled template value: {field}")
@@ -913,42 +932,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
@@ -1068,9 +1089,8 @@ 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]
@@ -1097,8 +1117,6 @@ class PhotoTemplate:
filename, funcname = subfield.split("::")
print(filename, funcname)
if not pathlib.Path(filename).is_file():
raise ValueError(f"'{filename}' does not appear to be a file")
@@ -1120,6 +1138,35 @@ class PhotoTemplate:
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)

View File

@@ -74,7 +74,7 @@ Filter:
;
FILTER_WORD:
/[\.\w]+/
/[\.\w:\/]+/
;
Conditional:

83
osxphotos/queryoptions.py Normal file
View 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)

View File

@@ -373,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

View File

@@ -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,7 +56,7 @@ 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

View File

@@ -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,

View File

@@ -1,9 +1,120 @@
""" pytest test configuration """
import pytest
import os
import pathlib
import pytest
from photoscript.utils import ditto
import osxphotos
from applescript import AppleScript
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") and TEST_LIBRARY is not None:
# --addalbum given in cli: do not skip addalbum tests (these require interactive test)
return
skip_addalbum = pytest.mark.skip(
reason="need --addalbum option and MacOS Catalina 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)

File diff suppressed because one or more lines are too long

17
tests/template_filter.py Normal file
View 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

View File

@@ -30,6 +30,8 @@ KEYWORDS_DICT = {
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 1}
UUID_DICT = {"derivatives": "FPm+ICxpQV+LPBKR22UepA"}
def test_init():
import osxphotos
@@ -162,3 +164,19 @@ def test_keyword_not_in_album():
photos3 = [p for p in photos2 if p not in photos1]
assert len(photos3) == 1
assert photos3[0].uuid == "Pj99JmYjQkeezdY2OFuSaw"
def test_path_derivatives():
# test path_derivatives
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["derivatives"]])
p = photos[0]
derivs = [
"/resources/proxies/derivatives/00/00/9/UNADJUSTEDRAW_thumb_9.jpg",
"/resources/proxies/derivatives/00/00/9/UNADJUSTEDRAW_mini_9.jpg",
]
for i, p in enumerate(p.path_derivatives):
assert p.endswith(derivs[i])

View File

@@ -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"]
@@ -486,6 +496,21 @@ def test_path_edited2(photosdb):
assert path is None
def test_path_derivatives(photosdb):
# test an path_derivatives
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
assert len(photos) == 1
p = photos[0]
path = p.path_derivatives
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])
def test_count(photosdb):
photos = photosdb.photos()
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
@@ -848,7 +873,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 +1125,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

View File

@@ -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"]
@@ -540,6 +556,21 @@ def test_path_edited2(photosdb):
assert path is None
def test_path_derivatives(photosdb):
# test an path_derivatives
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
assert len(photos) == 1
p = photos[0]
path = p.path_derivatives
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])
def test_ismovie(photosdb):
# test ismovie == True
@@ -890,7 +921,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 +1232,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 """

View File

@@ -24,14 +24,24 @@ 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" # in my personal library
BURST_ALBUM_FILES = [
"IMG_9812.JPG",
"IMG_9813.JPG",
"IMG_9814.JPG",
"IMG_9815.JPG",
"IMG_9816.JPG",
]
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"
@@ -1219,16 +1229,12 @@ def test_export_exiftool_path():
import shutil
import tempfile
from osxphotos.cli import export
from osxphotos.exiftool import ExifTool, get_exiftool_path
from osxphotos.exiftool import ExifTool
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
tempdir = tempfile.TemporaryDirectory()
exiftool_source = get_exiftool_path()
exiftool_path = os.path.join(tempdir.name, "myexiftool")
shutil.copy2(exiftool_source, exiftool_path)
for uuid in CLI_EXIFTOOL:
result = runner.invoke(
export,
@@ -1240,11 +1246,11 @@ def test_export_exiftool_path():
"--uuid",
f"{uuid}",
"--exiftool-path",
exiftool_path,
exiftool,
],
)
assert result.exit_code == 0
assert f"exiftool path: {exiftool_path}" in result.output
assert f"exiftool path: {exiftool}" in result.output
files = glob.glob("*")
assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]])
@@ -1280,9 +1286,6 @@ def test_export_exiftool_path_render_template():
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
tempdir = tempfile.TemporaryDirectory()
exiftool_path = os.path.join(tempdir.name, "myexiftool")
shutil.copy2(exiftool_source, exiftool_path)
for uuid in CLI_EXIFTOOL:
result = runner.invoke(
export,
@@ -1295,7 +1298,7 @@ def test_export_exiftool_path_render_template():
"--uuid",
f"{uuid}",
"--exiftool-path",
exiftool_path,
exiftool,
],
)
assert result.exit_code == 0
@@ -5724,27 +5727,25 @@ def test_export_burst_folder_album():
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_RHET),
".",
"-V",
"--directory",
"{folder_album}",
"--uuid",
UUID_BURST_ALBUM,
"--download-missing",
"--use-photokit",
],
)
assert result.exit_code == 0
folder_album = pathlib.Path("TestBurst")
assert folder_album.is_dir()
for filename in BURST_ALBUM_FILES:
path = folder_album / filename
assert path.is_file()
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():
@@ -5861,3 +5862,243 @@ def test_bad_query_eval():
)
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

View 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()))

View File

@@ -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
@@ -408,3 +417,25 @@ def test_photoinfo_exiftool_none():
photo = photosdb.photos(uuid=[uuid])[0]
exiftool = photo.exiftool
assert exiftool is None
def test_exiftool_terminate():
""" Test that exiftool process is terminated when exiftool.terminate() is called """
import osxphotos.exiftool
import subprocess
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
ps = subprocess.run(["ps"], capture_output=True)
stdout = ps.stdout.decode("utf-8")
assert "exiftool -stay_open" in stdout
osxphotos.exiftool.terminate_exiftool()
ps = subprocess.run(["ps"], capture_output=True)
stdout = ps.stdout.decode("utf-8")
assert "exiftool -stay_open" not in stdout
# verify we can create a new instance after termination
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif2.asdict()["IPTC:Keywords"] == "wedding"

View 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

View File

@@ -19,29 +19,34 @@ UUID_DICT = {
UUID_BURSTS = {
"9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2": {
"selected": True,
"filename": "IMAGE_9812.JPG",
"albums": ["TestBurst"],
},
"964F457D-5FFC-47B9-BEAD-56B0A83FEF63": {
"selected": True,
"filename": "IMG_9816.JPG",
"albums": [],
"selected": False,
"filename": "IMG_9812.JPG",
"burst_albums": ["TestBurst"],
"albums": ["TestBurst"]
},
"A385FA13-DF8E-482F-A8C5-970EDDF54C2F": {
"selected": False,
"filename": "IMG_9813.JPG",
"albums": ["TestBurst", "TestBurst2"],
"burst_albums": ["TestBurst"],
"albums": []
},
"38F8F30C-FF6D-49DA-8092-18497F1D6628": {
"selected": True,
"filename": "IMG_9814.JPG",
"albums": ["TestBurst2"],
"burst_albums": ["TestBurst", "TestBurst2"],
"albums": ["TestBurst2"]
},
"E3863443-9EA8-417F-A90B-8F7086623DAD": {
"selected": False,
"filename": "IMG_9815.JPG",
"albums": ["TestBurst", "TestBurst2"],
"burst_albums": ["TestBurst"],
"albums": []
},
"964F457D-5FFC-47B9-BEAD-56B0A83FEF63": {
"selected": True,
"filename": "IMG_9816.JPG",
"burst_albums": ["TestBurst"],
"albums": []
},
}
@@ -193,4 +198,5 @@ def test_burst_albums(photosdb):
photo = photosdb.get_photo(uuid)
assert photo.burst
assert photo.burst_selected == UUID_BURSTS[uuid]["selected"]
assert sorted(photo.burst_albums) == sorted(UUID_BURSTS[uuid]["albums"])
assert sorted(photo.albums) == sorted(UUID_BURSTS[uuid]["albums"])
assert sorted(photo.burst_albums) == sorted(UUID_BURSTS[uuid]["burst_albums"])

View File

@@ -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=[])"
)

View File

@@ -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(
@@ -410,6 +419,20 @@ def test_path_edited1(photosdb):
assert os.path.exists(path)
def test_path_derivatives(photosdb):
# test path_derivatives (not currently implemented for Photos <= 4)
import os.path
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
p = photos[0]
derivs = [
"/resources/proxies/derivatives/00/00/1/Y6OofYkbR96spbS6XgwOQw_thumb_1.jpg",
"/resources/proxies/derivatives/00/00/1/Y6OofYkbR96spbS6XgwOQw_mini_1.jpg",
]
for i, p in enumerate(p.path_derivatives):
assert p.endswith(derivs[i])
def test_path_edited2(photosdb):
# test an invalid edited path
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
@@ -651,4 +674,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

View File

@@ -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,
},
}

View File

@@ -87,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",
}
@@ -830,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"
@@ -982,3 +987,31 @@ def test_function_bad(photosdb):
"{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}"
)

View File

@@ -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)