Compare commits

...

12 Commits

Author SHA1 Message Date
Rhet Turnbull
b4bc906b6a Added --query-eval, implements #280 2021-04-08 22:15:58 -07:00
Rhet Turnbull
520a15fac6 Updated CHANGELOG.md, [skip ci] 2021-04-08 18:32:53 -07:00
Rhet Turnbull
032dff8967 Bug fix for #414, exiftool str replace 2021-04-05 13:11:17 -07:00
Rhet Turnbull
3c36b0fb33 Updated CHANGELOG.md, [skip ci] 2021-04-03 21:07:41 -07:00
Rhet Turnbull
d51d7a41e4 Added --name to search filename, closes #249, #412 2021-04-03 20:23:03 -07:00
Rhet Turnbull
60c926fea5 Updated CHANGELOG.md, [skip ci] 2021-04-03 08:03:10 -07:00
Rhet Turnbull
db27aac14b Added test for #409 2021-04-02 21:44:45 -07:00
Rhet Turnbull
d17454772c Update phototemplate.py
Fix for non-str values in exiftool template (#409)
2021-03-30 07:51:34 -06:00
dependabot[bot]
9c9e73ba96 Bump pygments from 2.6.1 to 2.7.4 (#408)
Bumps [pygments](https://github.com/pygments/pygments) from 2.6.1 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.6.1...2.7.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 07:37:28 -06:00
Rhet Turnbull
e21a78c2b3 Removed logging.debug code 2021-03-28 07:05:29 -07:00
Rhet Turnbull
de0fbf2bb9 Updated CHANGELOG.md, [skip ci] 2021-03-28 06:44:53 -07:00
Rhet Turnbull
b330e27fb8 Added --retry, issue #406 2021-03-27 22:40:56 -07:00
25 changed files with 501 additions and 157 deletions

View File

@@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.41.9](https://github.com/RhetTbull/osxphotos/compare/v0.41.8...v0.41.9)
> 5 April 2021
- Bug fix for #414, exiftool str replace [`032dff8`](https://github.com/RhetTbull/osxphotos/commit/032dff89677f049a234d9f498951b8b402d1b31c)
#### [v0.41.8](https://github.com/RhetTbull/osxphotos/compare/v0.41.7...v0.41.8)
> 4 April 2021
- Added --name to search filename, closes #249, #412 [`#249`](https://github.com/RhetTbull/osxphotos/issues/249)
#### [v0.41.7](https://github.com/RhetTbull/osxphotos/compare/v0.41.6...v0.41.7)
> 3 April 2021
- Bump pygments from 2.6.1 to 2.7.4 [`#408`](https://github.com/RhetTbull/osxphotos/pull/408)
- Removed logging.debug code [`e21a78c`](https://github.com/RhetTbull/osxphotos/commit/e21a78c2b39ee82610394b447a9aa697e489c3e4)
- Added test for #409 [`db27aac`](https://github.com/RhetTbull/osxphotos/commit/db27aac14bbaff0b2db44f8b2d41022ebcad18a7)
- Update phototemplate.py [`d174547`](https://github.com/RhetTbull/osxphotos/commit/d17454772cebbd6edd5d8e0f04e80feecbdb2355)
#### [v0.41.6](https://github.com/RhetTbull/osxphotos/compare/v0.41.5...v0.41.6)
> 28 March 2021
- Added --retry, issue #406 [`b330e27`](https://github.com/RhetTbull/osxphotos/commit/b330e27fb838b702cefcbdb588c2fbb924b4cbc4)
#### [v0.41.5](https://github.com/RhetTbull/osxphotos/compare/v0.41.4...v0.41.5)
> 27 March 2021
- Bump pyyaml from 5.1.2 to 5.4 [`#402`](https://github.com/RhetTbull/osxphotos/pull/402)
- Fixed albums for burst images, closes #401, #403, #404 [`#401`](https://github.com/RhetTbull/osxphotos/issues/401)
#### [v0.41.4](https://github.com/RhetTbull/osxphotos/compare/v0.41.3...v0.41.4)
> 22 March 2021

View File

@@ -203,6 +203,11 @@ Options:
searches top level folders (e.g. does not look
at subfolders)
--name FILENAME Search for photos with filename matching
FILENAME. If more than one --name options is
specified, they are treated as "OR", e.g. find
photos matching any FILENAME.
--uuid UUID Search for photos with UUID(s).
--uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines
@@ -310,6 +315,19 @@ Options:
albums.
--not-in-album Search for photos that are not in any albums.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following
list comprehension: `photos = [photo for photo
in photos if CRITERIA]` where photo represents
a PhotoInfo object. For example: `--query-eval
photo.favorite` returns all photos that have
been favorited and is equivalent to
--favorite. You may specify more than one
CRITERIA by using --query-eval multiple times.
See https://rhettbull.github.io/osxphotos/ for
additional documentation on the PhotoInfo
class.
--missing Export only photos missing from the Photos
library; must be used with --download-missing.
@@ -366,6 +384,11 @@ Options:
may create name collisions on export. (e.g. if
two files happen to have the same name)
--retry RETRY Automatically retry export up to RETRY times
if an error occurs during export. This may be
useful with network drives that experience
intermittent errors.
--export-by-date Automatically create output folders to
organize photos by date created (e.g.
DEST/2019/12/20/photoname.jpg).

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: c43f566654ff6a66a64cd55da2e67fef
config: 83db317a0b058d0bba826496215f3269
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.41.4 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>

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.4 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>

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.4 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -486,9 +486,24 @@
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_albums</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_albums</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_albums</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_albums</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photos is contained in &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photo is contained in &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -498,6 +513,21 @@
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_album_info</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">album_info</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_album_info</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; ImportInfo object representing import session for the photo or None if no import session &quot;&quot;&quot;</span>
@@ -713,6 +743,11 @@
<span class="sd">&quot;&quot;&quot; Returns True if photo is part of a Burst photo set, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_selected</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst_key&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is a burst photo, returns list of PhotoInfo objects</span>

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.41.4',
VERSION: '0.41.6',
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.41.4 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.41.6 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>
@@ -549,6 +549,12 @@ to modify this behavior.</p>
<dd><p>Overwrite existing files. Default behavior is to add (1), (2), etc to filename if file already exists. Use this with caution as it may create name collisions on export. (e.g. if two files happen to have the same name)</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-retry">
<code class="sig-name descname"><span class="pre">--retry</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;RETRY&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-retry" title="Permalink to this definition"></a></dt>
<dd><p>Automatically retry export up to RETRY times if an error occurs during export. This may be useful with network drives that experience intermittent errors.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-export-by-date">
<code class="sig-name descname"><span class="pre">--export-by-date</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-export-by-date" title="Permalink to this definition"></a></dt>

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.41.4 documentation</title>
<title>Index &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -538,8 +538,6 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
--no-likes
@@ -549,6 +547,8 @@
<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
@@ -799,6 +799,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--retry &lt;RETRY&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
@@ -1089,13 +1096,19 @@
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_selected">burst_selected() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
@@ -1586,6 +1599,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report &lt;path to export report&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">--retry &lt;RETRY&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a>
</li>

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.41.4 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>

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.41.4 documentation</title>
<title>osxphotos &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/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.

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos package &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos package &#8212; osxphotos 0.41.6 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>
@@ -650,7 +650,7 @@ including keywords, persons, albums, uuid, path, etc.</p>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.album_info">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">album_info</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.album_info" title="Permalink to this definition"></a></dt>
<dd><p>list of AlbumInfo objects representing albums the photos is contained in</p>
<dd><p>list of AlbumInfo objects representing albums the photo is contained in</p>
</dd></dl>
<dl class="py method">
@@ -671,6 +671,18 @@ including keywords, persons, albums, uuid, path, etc.</p>
<dd><p>Returns True if photo is part of a Burst photo set, otherwise False</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_album_info">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_album_info</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_album_info" title="Permalink to this definition"></a></dt>
<dd><p>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</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_albums">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_albums</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_albums" title="Permalink to this definition"></a></dt>
<dd><p>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</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_photos">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_photos</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_photos" title="Permalink to this definition"></a></dt>
@@ -679,6 +691,12 @@ that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_selected">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_selected</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_selected" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.comments">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">comments</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.comments" title="Permalink to this definition"></a></dt>

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.41.4 documentation</title>
<title>Search &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.41.5"
__version__ = "0.41.10"

View File

@@ -238,6 +238,15 @@ def query_options(f):
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
"Only searches top level folders (e.g. does not look at subfolders)",
),
o(
"--name",
metavar="FILENAME",
default=None,
multiple=True,
help="Search for photos with filename matching FILENAME. "
'If more than one --name options is specified, they are treated as "OR", '
"e.g. find photos matching any FILENAME. ",
),
o(
"--uuid",
metavar="UUID",
@@ -438,6 +447,19 @@ def query_options(f):
is_flag=True,
help="Search for photos that are not in any albums.",
),
o(
"--query-eval",
metavar="CRITERIA",
multiple=True,
help="Evaluate CRITERIA to filter photos. "
"CRITERIA will be evaluated in context of the following list comprehension: "
"`photos = [photo for photo in photos if CRITERIA]` "
"where photo represents a PhotoInfo object. "
"For example: `--query-eval photo.favorite` returns all photos that have been "
"favorited and is equivalent to --favorite. "
"You may specify more than one CRITERIA by using --query-eval multiple times. "
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
),
]
for o in options[::-1]:
f = o(f)
@@ -517,6 +539,13 @@ def cli(ctx, db, json_, debug):
"Use this with caution as it may create name collisions on export. "
"(e.g. if two files happen to have the same name)",
)
@click.option(
"--retry",
metavar="RETRY",
type=click.INT,
help="Automatically retry export up to RETRY times if an error occurs during export. "
"This may be useful with network drives that experience intermittent errors.",
)
@click.option(
"--export-by-date",
is_flag=True,
@@ -864,6 +893,7 @@ def export(
album,
folder,
uuid,
name,
uuid_from_file,
title,
no_title,
@@ -892,6 +922,7 @@ def export(
export_as_hardlink,
touch_file,
overwrite,
retry,
export_by_date,
skip_edited,
skip_original_if_edited,
@@ -966,6 +997,7 @@ def export(
beta,
in_album,
not_in_album,
query_eval,
):
"""Export photos from the Photos database.
Export path DEST is required.
@@ -1012,6 +1044,7 @@ def export(
person = cfg.person
album = cfg.album
folder = cfg.folder
name = cfg.name
uuid = cfg.uuid
uuid_from_file = cfg.uuid_from_file
title = cfg.title
@@ -1040,6 +1073,7 @@ def export(
export_as_hardlink = cfg.export_as_hardlink
touch_file = cfg.touch_file
overwrite = cfg.overwrite
retry = cfg.retry
export_by_date = cfg.export_by_date
skip_edited = cfg.skip_edited
skip_original_if_edited = cfg.skip_original_if_edited
@@ -1111,6 +1145,7 @@ def export(
only_new = cfg.only_new
in_album = cfg.in_album
not_in_album = cfg.not_in_album
query_eval = cfg.query_eval
# config file might have changed verbose
VERBOSE = bool(verbose)
@@ -1199,6 +1234,7 @@ def export(
original_suffix = (
DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix
)
retry = 0 if not retry else retry
if not os.path.isdir(dest):
click.echo(
@@ -1411,6 +1447,8 @@ def export(
burst_photos=export_bursts,
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
missing_bursts=(download_missing and use_photokit) or not download_missing,
name=name,
query_eval=query_eval,
)
if photos:
@@ -1475,6 +1513,7 @@ def export(
strip=strip,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
)
results += export_results
@@ -1634,6 +1673,7 @@ def query(
person,
album,
folder,
name,
uuid,
uuid_from_file,
title,
@@ -1694,6 +1734,7 @@ def query(
is_reference,
in_album,
not_in_album,
query_eval,
):
"""Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND"
@@ -1707,6 +1748,7 @@ def query(
person,
album,
folder,
name,
uuid,
uuid_from_file,
edited,
@@ -1719,6 +1761,7 @@ def query(
to_time,
label,
is_reference,
query_eval,
]
exclusive = [
(favorite, not_favorite),
@@ -1840,6 +1883,8 @@ def query(
is_reference=is_reference,
in_album=in_album,
not_in_album=not_in_album,
name=name,
query_eval=query_eval,
)
# below needed for to make CliRunner work for testing
@@ -2017,6 +2062,8 @@ def _query(
not_in_album=False,
burst_photos=None,
missing_bursts=None,
name=None,
query_eval=None,
):
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
@@ -2083,30 +2130,38 @@ def _query(
if title:
# search title field for text
# if more than one, find photos with all title values in title
photo_list = []
if ignore_case:
# case-insensitive
for t in title:
t = t.lower()
photos = [p for p in photos if p.title and t in p.title.lower()]
photo_list.extend(
[p for p in photos if p.title and t in p.title.lower()]
)
else:
for t in title:
photos = [p for p in photos if p.title and t in p.title]
photo_list.extend([p for p in photos if p.title and t in p.title])
photos = photo_list
elif 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 ignore_case:
# case-insensitive
for d in description:
d = d.lower()
photos = [
p for p in photos if p.description and d in p.description.lower()
]
photo_list.extend(
[p for p in photos if p.description and d in p.description.lower()]
)
else:
for d in description:
photos = [p for p in photos if p.description and d in p.description]
photo_list.extend(
[p for p in photos if p.description and d in p.description]
)
photos = photo_list
elif no_description:
photos = [p for p in photos if not p.description]
@@ -2281,6 +2336,38 @@ def _query(
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 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 query_eval:
for q in query_eval:
query_string = f"[photo for photo in photos if {q}]"
try:
photos = eval(query_string)
except Exception as e:
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {e}"
)
return photos
@@ -2353,6 +2440,7 @@ def export_photo(
strip=False,
jpeg_ext=None,
replace_keywords=False,
retry=0,
):
"""Helper function for export that does the actual export
@@ -2392,6 +2480,7 @@ def export_photo(
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
retry: retry up to retry # of times if there's an error
Returns:
list of path(s) of exported photo or None if photo was missing
@@ -2541,72 +2630,88 @@ def export_photo(
str(pathlib.Path(dest_path) / original_filename)
)
else:
try:
export_results = photo.export2(
dest_path,
original_filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
)
results += export_results
for warning_ in export_results.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
tries = 0
while tries <= retry:
tries += 1
error = 0
try:
export_results = photo.export2(
dest_path,
original_filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
)
for error_ in export_results.exiftool_error:
for warning_ in export_results.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
except Exception as e:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / original_filename), e)
)
if tries > retry:
results.error.append(
(str(pathlib.Path(dest) / original_filename), e)
)
break
else:
click.echo(
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
else:
verbose_(f"Skipping original version of {photo.original_filename}")
@@ -2690,70 +2795,87 @@ def export_photo(
)
else:
try:
export_results_edited = photo.export2(
dest_path,
edited_filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
edited=True,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
)
results += export_results_edited
for warning_ in export_results_edited.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
tries = 0
while tries <= retry:
tries += 1
error = 0
try:
export_results_edited = photo.export2(
dest_path,
edited_filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
edited=True,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
)
for error_ in export_results_edited.exiftool_error:
for warning_ in export_results_edited.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results_edited.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results_edited.error:
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results_edited
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results_edited.error:
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
except Exception as e:
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / edited_filename), e)
)
if tries > retry:
results.error.append(
(str(pathlib.Path(dest) / edited_filename), e)
)
break
else:
click.echo(
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
if verbose:
if update:

View File

@@ -1660,7 +1660,7 @@ def _exiftool_dict(
if type(val) == str:
exif[field] = val.replace("\n", " ")
elif type(val) == list:
exif[field] = [v.replace("\n", " ") for v in val]
exif[field] = [str(v).replace("\n", " ") for v in val if v is not None]
return exif

View File

@@ -1828,7 +1828,6 @@ class PhotosDB:
# get details about photos
verbose("Processing photo details.")
logging.debug(f"Getting information about photos")
c.execute(
f"""SELECT {asset_table}.ZUUID,
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
@@ -2736,8 +2735,6 @@ class PhotosDB:
# an empty album will be in _dbalbum_titles but not _dbalbums_album
pass
album_set.update(title_set)
else:
logging.debug(f"Could not find album '{album}' in database")
photos_sets.append(album_set)
if uuid:
@@ -2745,8 +2742,6 @@ class PhotosDB:
for u in uuid:
if u in self._dbphotos:
uuid_set.update([u])
else:
logging.debug(f"Could not find uuid '{u}' in database")
photos_sets.append(uuid_set)
if keywords:
@@ -2754,8 +2749,6 @@ class PhotosDB:
for keyword in keywords:
if keyword in self._dbkeywords_keyword:
keyword_set.update(self._dbkeywords_keyword[keyword])
else:
logging.debug(f"Could not find keyword '{keyword}' in database")
photos_sets.append(keyword_set)
if persons:
@@ -2768,8 +2761,6 @@ class PhotosDB:
except KeyError:
# some persons have zero photos so they won't be in _dbfaces_pk
pass
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set)
if from_date or to_date: # sourcery off
@@ -2780,14 +2771,10 @@ class PhotosDB:
dsel = {
k: v for k, v in dsel.items() if v["imageDate"] >= from_date
}
logging.debug(
f"Found %i items with from_date {from_date}" % len(dsel)
)
if to_date:
if not datetime_has_tz(to_date):
to_date = datetime_naive_to_local(to_date)
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date}
logging.debug(f"Found %i items with to_date {to_date}" % len(dsel))
photos_sets.append(set(dsel.keys()))
photoinfo = []

View File

@@ -918,6 +918,7 @@ class PhotoTemplate:
if subfield in exifdict:
values = exifdict[subfield]
values = [values] if not isinstance(values, list) else values
values = [str(v) for v in values]
# sanitize directory names if needed
if filename:

View File

@@ -59,7 +59,7 @@ py==1.8.0
py2app==0.21
pycparser==2.20
pyfiglet==0.8.post1
Pygments==2.6.1
Pygments==2.7.4
PyInstaller==3.6
pyinstaller-setuptools==2019.3
pylint==2.3.1

File diff suppressed because one or more lines are too long

View File

@@ -5699,3 +5699,103 @@ def test_export_burst_folder_album():
path = folder_album / filename
assert path.is_file()
def test_query_name():
""" test query --name """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "DSC03584"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_query_name_i():
""" test query --name -i """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--name",
"dsc03584",
"-i",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_export_name():
""" test export --name """
import glob
import os
import os.path
import osxphotos
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--name", "DSC03584"]
)
assert result.exit_code == 0
files = glob.glob("*")
assert len(files) == 1
def test_query_eval():
""" test export --query-eval """
import glob
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--query-eval", "'DSC03584' in photo.original_filename"]
)
assert result.exit_code == 0
files = glob.glob("*")
assert len(files) == 1
def test_bad_query_eval():
""" test export --query-eval with bad input """
import glob
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--query-eval", "'DSC03584' in photo.originalfilename"]
)
assert result.exit_code != 0
assert "Error: Invalid query-eval CRITERIA" in result.output

View File

@@ -133,6 +133,9 @@ UUID_EXIFTOOL = {
"England,London,London 2018,St. James's Park,UK,United Kingdom"
],
},
"1EB2B765-0765-43BA-A90C-0D0580E6172C": {
"{exiftool:EXIF:SubSecTimeOriginal}": ["22"]
},
}
TEMPLATE_VALUES = {