Compare commits

...

10 Commits

Author SHA1 Message Date
Rhet Turnbull
abbb200838 Fixed path_derivatives for shared photos, #687 2022-05-08 18:30:13 -07:00
Rhet Turnbull
e7eefce5c5 Added --no-keyword, #637 2022-05-07 14:24:06 -07:00
Rhet Turnbull
2ed6e11426 Fixed typo in docs 2022-05-07 10:00:51 -07:00
Rhet Turnbull
4096054abb Updated CHANGELOG.md [skip ci] 2022-05-07 09:15:32 -07:00
Rhet Turnbull
5ab5c53b26 Added --limit, version bump 2022-05-07 09:11:21 -07:00
Rhet Turnbull
35a4777ae4 Added --limit, #592 (#685) 2022-05-07 08:59:01 -07:00
Rhet Turnbull
9fddbefa66 Updated CHANGELOG.md [skip ci] 2022-05-06 21:33:08 -07:00
Rhet Turnbull
ff2e810d49 Version bump, updated docs 2022-05-06 21:26:27 -07:00
Rhet Turnbull
026e90a8ed Feature metadata changed 621 (#684)
* Added metadata_changed to ExportResults, #621

* Updated docs
2022-05-06 21:05:59 -07:00
Rhet Turnbull
95103f7c8d Updated CHANGELOG.md [skip ci] 2022-05-05 17:40:45 -07:00
42 changed files with 401 additions and 124 deletions

View File

@@ -1943,7 +1943,7 @@ cog.out(get_template_field_table())
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.48.3'|
|{osxphotos_version}|The osxphotos version, e.g. '0.48.6'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -4,6 +4,32 @@ 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.48.6](https://github.com/RhetTbull/osxphotos/compare/v0.48.5...v0.48.6)
> 7 May 2022
- Added --limit, version bump [`5ab5c53`](https://github.com/RhetTbull/osxphotos/commit/5ab5c53b26207eb326191c110ac544592c980c32)
#### [v0.48.5](https://github.com/RhetTbull/osxphotos/compare/v0.48.4...v0.48.5)
> 7 May 2022
- Added --limit, #592 [`#685`](https://github.com/RhetTbull/osxphotos/pull/685)
#### [v0.48.4](https://github.com/RhetTbull/osxphotos/compare/v0.48.3...v0.48.4)
> 6 May 2022
- Feature metadata changed 621 [`#684`](https://github.com/RhetTbull/osxphotos/pull/684)
- Version bump, updated docs [`ff2e810`](https://github.com/RhetTbull/osxphotos/commit/ff2e810d49f9acffb81f3b7aea6271191d6762f9)
#### [v0.48.3](https://github.com/RhetTbull/osxphotos/compare/v0.48.2...v0.48.3)
> 5 May 2022
- Added --added-after, --added-before, --added-in-last, #439 [`#683`](https://github.com/RhetTbull/osxphotos/pull/683)
- Updated to pytimeparse2, added tests for custom Click param types [`3ed658a`](https://github.com/RhetTbull/osxphotos/commit/3ed658a7d018b5ac61c1054e20a59576e8613609)
#### [v0.48.2](https://github.com/RhetTbull/osxphotos/compare/v0.48.1...v0.48.2)
> 3 May 2022

View File

@@ -882,6 +882,9 @@ Options:
exported files, even if missing from the
export folder and only exports new files that
haven't previously been exported.
--limit LIMIT Export at most LIMIT photos. Useful for
testing. May be used with --update to export
incrementally.
--dry-run Dry run (test) the export but don't actually
export any files; most useful with --verbose.
--export-as-hardlink Hardlink files instead of copying them. Cannot
@@ -1831,7 +1834,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.48.3'
{osxphotos_version} The osxphotos version, e.g. '0.48.6'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified
@@ -2981,6 +2984,9 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates.
#### `hexdigest`
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
#### `json()`
Returns a JSON representation of all photo info.
@@ -3994,7 +4000,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.48.3'|
|{osxphotos_version}|The osxphotos version, e.g. '0.48.6'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -4174,6 +4180,7 @@ Attributes:
* exiftool_error: list of errors generated by exiftool during export
* xattr_written: list of files with extended attributes written during export
* xattr_skipped: list of files where extended attributes were skipped when update=True
* metadata_changed: list of files where metadata changed since last export
* deleted_files: reserved for use by osxphotos CLI
* deleted_directories: reserved for use by osxphotos CLI
* exported_album: reserved for use by osxphotos CLI

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: 7bb2c9249a5178c80045917234b5db0f
config: 7c9b3fd4a18d08287a7ba12c4e147bf5
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../genindex.html" /><link rel="search" title="Search" href="../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>Overview: module code - osxphotos 0.48.3 documentation</title>
<title>Overview: module code - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="../index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.photoexporter - osxphotos 0.47.9 documentation</title>
<title>osxphotos.photoexporter - osxphotos 0.48.3 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.47.9 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.47.9 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -155,11 +155,12 @@
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">osxphotos Tutorial</a></li>
<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="../../package_overview.html">Example uses of the python package</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">osxphotos python API</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></li>
<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="../../template_help.html">OSXPhotos Template System</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
</ul>
</div>
@@ -197,13 +198,11 @@
<span class="sd">"""</span>
<span class="kn">import</span> <span class="nn">dataclasses</span>
<span class="kn">import</span> <span class="nn">hashlib</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">tempfile</span>
<span class="kn">import</span> <span class="nn">typing</span> <span class="k">as</span> <span class="nn">t</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span> <span class="c1"># pylint: disable=syntax-error</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">asdict</span><span class="p">,</span> <span class="n">dataclass</span>
@@ -240,14 +239,13 @@
<span class="kn">from</span> <span class="nn">.phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">.rich_utils</span> <span class="kn">import</span> <span class="n">add_rich_markup_tag</span>
<span class="kn">from</span> <span class="nn">.uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">hexdigest</span><span class="p">,</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
<span class="s2">"ExportError"</span><span class="p">,</span>
<span class="s2">"ExportOptions"</span><span class="p">,</span>
<span class="s2">"ExportResults"</span><span class="p">,</span>
<span class="s2">"PhotoExporter"</span><span class="p">,</span>
<span class="s2">"hexdigest"</span><span class="p">,</span>
<span class="s2">"rename_jpeg_files"</span><span class="p">,</span>
<span class="p">]</span>
@@ -461,6 +459,7 @@
<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="n">metadata_changed</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>
@@ -487,6 +486,7 @@
<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="bp">self</span><span class="o">.</span><span class="n">metadata_changed</span> <span class="o">=</span> <span class="n">metadata_changed</span> <span class="ow">or</span> <span class="p">[]</span>
<div class="viewcode-block" id="ExportResults.all_files"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportResults.all_files">[docs]</a> <span class="k">def</span> <span class="nf">all_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""return all filenames contained in results"""</span>
@@ -537,6 +537,7 @@
<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="bp">self</span><span class="o">.</span><span class="n">metadata_changed</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">metadata_changed</span>
<span class="k">return</span> <span class="bp">self</span>
@@ -566,12 +567,14 @@
<span class="o">+</span> <span class="sa">f</span><span class="s2">",exported_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exported_album</span><span class="si">}</span><span class="s2">"</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">",skipped_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">skipped_album</span><span class="si">}</span><span class="s2">"</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">",missing_album=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">missing_album</span><span class="si">}</span><span class="s2">"</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">",metadata_changed=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">metadata_changed</span><span class="si">}</span><span class="s2">"</span>
<span class="o">+</span> <span class="s2">")"</span>
<span class="p">)</span></div>
<div class="viewcode-block" id="PhotoExporter"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoExporter">[docs]</a><span class="k">class</span> <span class="nc">PhotoExporter</span><span class="p">:</span>
<span class="sd">"""Export a photo"""</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">photo</span><span class="p">:</span> <span class="s2">"PhotoInfo"</span><span class="p">,</span> <span class="n">tmpdir</span><span class="p">:</span> <span class="n">t</span><span class="o">.</span><span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">photo</span> <span class="o">=</span> <span class="n">photo</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_render_options</span> <span class="o">=</span> <span class="n">RenderOptions</span><span class="p">()</span>
@@ -934,7 +937,7 @@
<span class="k">return</span> <span class="n">ShouldUpdate</span><span class="o">.</span><span class="n">EDITED_SIG_DIFFERENT</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">force_update</span><span class="p">:</span>
<span class="n">current_digest</span> <span class="o">=</span> <span class="n">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">json</span><span class="p">())</span>
<span class="n">current_digest</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">hexdigest</span>
<span class="k">if</span> <span class="n">current_digest</span> <span class="o">!=</span> <span class="n">file_record</span><span class="o">.</span><span class="n">digest</span><span class="p">:</span>
<span class="c1"># metadata in Photos changed, force update</span>
<span class="k">return</span> <span class="n">ShouldUpdate</span><span class="o">.</span><span class="n">DIGEST_DIFFERENT</span>
@@ -1374,8 +1377,9 @@
<span class="n">rec</span><span class="o">.</span><span class="n">dest_sig</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">exiftool</span><span class="p">:</span>
<span class="n">rec</span><span class="o">.</span><span class="n">exifdata</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool_json_sidecar</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">force_update</span><span class="p">:</span>
<span class="n">rec</span><span class="o">.</span><span class="n">digest</span> <span class="o">=</span> <span class="n">hexdigest</span><span class="p">(</span><span class="n">photoinfo</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">hexdigest</span> <span class="o">!=</span> <span class="n">rec</span><span class="o">.</span><span class="n">digest</span><span class="p">:</span>
<span class="n">results</span><span class="o">.</span><span class="n">metadata_changed</span> <span class="o">=</span> <span class="p">[</span><span class="n">dest_str</span><span class="p">]</span>
<span class="n">rec</span><span class="o">.</span><span class="n">digest</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">hexdigest</span>
<span class="k">return</span> <span class="n">results</span>
@@ -2206,13 +2210,6 @@
<span class="n">f</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></div>
<span class="k">def</span> <span class="nf">hexdigest</span><span class="p">(</span><span class="n">strval</span><span class="p">):</span>
<span class="sd">"""hexdigest of a string, using blake2b"""</span>
<span class="n">h</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">blake2b</span><span class="p">(</span><span class="n">digest_size</span><span class="o">=</span><span class="mi">20</span><span class="p">)</span>
<span class="n">h</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="nb">bytes</span><span class="p">(</span><span class="n">strval</span><span class="p">,</span> <span class="s2">"utf-8"</span><span class="p">))</span>
<span class="k">return</span> <span class="n">h</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_check_export_suffix</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">,</span> <span class="n">edited</span><span class="p">):</span>
<span class="sd">"""Helper function for exporting photos to check file extensions of destination path.</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.photoinfo - osxphotos 0.48.1 documentation</title>
<title>osxphotos.photoinfo - osxphotos 0.48.3 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.48.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.48.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -207,6 +207,7 @@
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cached_property</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">import</span> <span class="nn">yaml</span>
@@ -250,7 +251,7 @@
<span class="kn">from</span> <span class="nn">.searchinfo</span> <span class="kn">import</span> <span class="n">SearchInfo</span>
<span class="kn">from</span> <span class="nn">.text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
<span class="kn">from</span> <span class="nn">.uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">hexdigest</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PhotoInfo"</span><span class="p">,</span> <span class="s2">"PhotoInfoNone"</span><span class="p">]</span>
@@ -1552,6 +1553,12 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span> <span class="o">=</span> <span class="n">exiftool</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span>
<span class="nd">@cached_property</span>
<span class="k">def</span> <span class="nf">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""Returns a unique digest of the photo's properties and metadata;</span>
<span class="sd"> useful for detecting changes in any property/metadata of the photo"""</span>
<span class="k">return</span> <span class="n">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">json</span><span class="p">())</span>
<div class="viewcode-block" id="PhotoInfo.detected_text"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.detected_text">[docs]</a> <span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
<span class="sd">"""Detects text in photo and returns lists of results as (detected text, confidence)</span>

View File

@@ -320,7 +320,7 @@ Template Substitutions
* - {crlf}
- a carriage return + line feed: '\r\n'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.48.3'
- The osxphotos version, e.g. '0.48.6'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}

View File

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

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Template System" href="template_help.html" /><link rel="prev" title="OSXPhotos Tutorial" href="tutorial.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.48.3 documentation</title>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -765,6 +765,11 @@ to modify this behavior.</p>
<dd><p>If used with update, ignores any previously exported files, even if missing from the export folder and only exports new files that havent previously been exported.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-export-limit">
<span class="sig-name descname"><span class="pre">--limit</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;LIMIT&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-export-limit" title="Permalink to this definition">#</a></dt>
<dd><p>Export at most LIMIT photos. Useful for testing. Maybe used with update to export incrementally.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-export-dry-run">
<span class="sig-name descname"><span class="pre">--dry-run</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-export-dry-run" title="Permalink to this definition">#</a></dt>
<dd><p>Dry run (test) the export but dont actually export any files; most useful with verbose.</p>

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Index - osxphotos 0.48.3 documentation</title>
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Index - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -122,7 +122,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -145,7 +145,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -877,6 +877,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-L">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--limit
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-limit">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
@@ -2335,6 +2342,8 @@
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotoInfo.height">height (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.hexdigest">hexdigest (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.hidden">hidden (osxphotos.PhotoInfo property)</a>
@@ -2799,6 +2808,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-keyword-template">--keyword-template</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-label">--label</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-limit">--limit</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-live">--live</a>
</li>

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos" href="overview.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos 0.48.3 documentation</title>
<title>osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="#"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

Binary file not shown.

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Tutorial" href="tutorial.html" /><link rel="prev" title="Welcome to OSXPhotoss documentation!" href="index.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos - osxphotos 0.48.3 documentation</title>
<title>OSXPhotos - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos python API" href="reference.html" /><link rel="prev" title="OSXPhotos Template System" href="template_help.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Python Package Overview - osxphotos 0.48.3 documentation</title>
<title>OSXPhotos Python Package Overview - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Python Module Index - osxphotos 0.48.3 documentation</title>
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Python Module Index - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -122,7 +122,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -145,7 +145,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Search - osxphotos 0.48.3 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Search - osxphotos 0.48.6 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
@@ -121,7 +121,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -144,7 +144,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Python Package Overview" href="package_overview.html" /><link rel="prev" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Template System - osxphotos 0.48.3 documentation</title>
<title>OSXPhotos Template System - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -567,7 +567,7 @@
<td><p>a carriage return + line feed: rn</p></td>
</tr>
<tr class="row-odd"><td><p>{osxphotos_version}</p></td>
<td><p>The osxphotos version, e.g. 0.48.3</p></td>
<td><p>The osxphotos version, e.g. 0.48.6</p></td>
</tr>
<tr class="row-even"><td><p>{osxphotos_cmd_line}</p></td>
<td><p>The full command line used to run osxphotos</p></td>

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" /><link rel="prev" title="OSXPhotos" href="overview.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Tutorial - osxphotos 0.48.3 documentation</title>
<title>OSXPhotos Tutorial - osxphotos 0.48.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.48.3 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.48.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.48.3 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.48.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -320,7 +320,7 @@ Template Substitutions
* - {crlf}
- a carriage return + line feed: '\r\n'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.48.3'
- The osxphotos version, e.g. '0.48.6'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}

View File

@@ -50,6 +50,7 @@ def post_function(
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
# metadata_changed: list of filenames that had metadata changes since last export
for filename in results.exported:
# do your processing here

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.48.3"
__version__ = "0.48.6"

View File

@@ -146,6 +146,11 @@ def QUERY_OPTIONS(f):
help="Search for photos with keyword KEYWORD. "
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
),
o(
"--no-keyword",
is_flag=True,
help="Search for photos with no keyword.",
),
o(
"--person",
metavar="PERSON",

View File

@@ -55,7 +55,7 @@ from osxphotos.photosalbum import PhotosAlbum
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.queryoptions import QueryOptions
from osxphotos.uti import get_preferred_uti_extension
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path, pluralize
from .click_rich_echo import (
rich_click_echo,
@@ -138,6 +138,13 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
help="If used with --update, ignores any previously exported files, even if missing from "
"the export folder and only exports new files that haven't previously been exported.",
)
@click.option(
"--limit",
metavar="LIMIT",
help="Export at most LIMIT photos. "
"Useful for testing. May be used with --update to export incrementally.",
type=int,
)
@click.option(
"--dry-run",
is_flag=True,
@@ -730,6 +737,7 @@ def export(
keyword_template,
keyword,
label,
limit,
live,
load_config,
location,
@@ -741,6 +749,7 @@ def export(
no_description,
no_likes,
no_location,
no_keyword,
no_place,
no_progress,
no_title,
@@ -945,6 +954,7 @@ def export(
keyword = cfg.keyword
keyword_template = cfg.keyword_template
label = cfg.label
limit = cfg.limit
live = cfg.live
location = cfg.location
max_size = cfg.max_size
@@ -955,6 +965,7 @@ def export(
no_description = cfg.no_description
no_likes = cfg.no_likes
no_location = cfg.no_location
no_keyword = cfg.no_keyword
no_place = cfg.no_place
no_progress = cfg.no_progress
no_title = cfg.no_title
@@ -1059,6 +1070,7 @@ def export(
("in_album", "not_in_album"),
("live", "not_live"),
("location", "no_location"),
("keyword", "no_keyword"),
("only_photos", "only_movies"),
("panorama", "not_panorama"),
("place", "no_place"),
@@ -1316,6 +1328,7 @@ def export(
no_description=no_description,
no_likes=no_likes,
no_location=no_location,
no_keyword=no_keyword,
no_place=no_place,
no_title=no_title,
not_burst=not_burst,
@@ -1383,8 +1396,7 @@ def export(
if photos:
num_photos = len(photos)
# TODO: photos or photo appears several times, pull into a separate function
photo_str = "photos" if num_photos > 1 else "photo"
photo_str = pluralize(num_photos, "photo", "photos")
rich_echo(
f"Exporting [num]{num_photos}[/num] {photo_str} to [filepath]{dest}[/]..."
)
@@ -1412,9 +1424,11 @@ def export(
)
photo_num = 0
num_exported = 0
limit_str = f" (limit = [num]{limit}[/num])" if limit else ""
with rich_progress(console=get_verbose_console(), mock=no_progress) as progress:
task = progress.add_task(
f"Exporting [num]{num_photos}[/] photos", total=num_photos
f"Exporting [num]{num_photos}[/] photos{limit_str}", total=num_photos
)
for p in photos:
photo_num += 1
@@ -1579,7 +1593,17 @@ def export(
progress.advance(task)
photo_str_total = "photos" if len(photos) != 1 else "photo"
# handle limit
if export_results.exported:
# if any photos were exported, increment num_exported used by limit
# limit considers each PhotoInfo object as a single photo even if multiple files are exported
num_exported += 1
if limit and num_exported >= limit:
# advance progress to end
progress.advance(task, num_photos - photo_num)
break
photo_str_total = pluralize(len(photos), "photo", "photos")
if update or force_update:
summary = (
f"Processed: [num]{len(photos)}[/] {photo_str_total}, "
@@ -1597,6 +1621,8 @@ def export(
summary += f"error: [num]{len(results.error)}[/]"
if touch_file:
summary += f", touched date: [num]{len(results.touched)}[/]"
if limit:
summary += f", limit: [num]{num_exported}[/]/[num]{limit}[/] exported"
rich_echo(summary)
stop_time = time.perf_counter()
rich_echo(f"Elapsed time: [time]{format_sec_to_hhmmss(stop_time-start_time)}")

View File

@@ -113,6 +113,7 @@ def query(
no_description,
no_likes,
no_location,
no_keyword,
no_place,
no_title,
not_burst,
@@ -197,6 +198,7 @@ def query(
(any(description), no_description),
(any(place), no_place),
(any(title), no_title),
(any(keyword), no_keyword),
(burst, not_burst),
(cloudasset, not_cloudasset),
(deleted, deleted_only),
@@ -293,6 +295,7 @@ def query(
no_description=no_description,
no_likes=no_likes,
no_location=no_location,
no_keyword=no_keyword,
no_place=no_place,
no_title=no_title,
not_burst=not_burst,

View File

@@ -284,6 +284,7 @@ def _query_options_from_kwargs(**kwargs) -> QueryOptions:
("incloud", "not_incloud"),
("live", "not_live"),
("location", "no_location"),
("keyword", "no_keyword"),
("missing", "not_missing"),
("only_photos", "only_movies"),
("panorama", "not_panorama"),
@@ -301,6 +302,7 @@ def _query_options_from_kwargs(**kwargs) -> QueryOptions:
all([any(kwargs["title"]), kwargs["no_title"]]),
all([any(kwargs["description"]), kwargs["no_description"]]),
all([any(kwargs["place"]), kwargs["no_place"]]),
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
]
):
raise IncompatibleQueryOptions

Binary file not shown.

View File

@@ -2,13 +2,11 @@
"""
import dataclasses
import hashlib
import json
import logging
import os
import pathlib
import re
import tempfile
import typing as t
from collections import namedtuple # pylint: disable=syntax-error
from dataclasses import asdict, dataclass
@@ -45,14 +43,13 @@ from .photokit import (
from .phototemplate import RenderOptions
from .rich_utils import add_rich_markup_tag
from .uti import get_preferred_uti_extension
from .utils import increment_filename, lineno, list_directory
from .utils import hexdigest, increment_filename, lineno, list_directory
__all__ = [
"ExportError",
"ExportOptions",
"ExportResults",
"PhotoExporter",
"hexdigest",
"rename_jpeg_files",
]
@@ -266,6 +263,7 @@ class ExportResults:
exported_album=None,
skipped_album=None,
missing_album=None,
metadata_changed=None,
):
self.exported = exported or []
self.new = new or []
@@ -292,6 +290,7 @@ class ExportResults:
self.exported_album = exported_album or []
self.skipped_album = skipped_album or []
self.missing_album = missing_album or []
self.metadata_changed = metadata_changed or []
def all_files(self):
"""return all filenames contained in results"""
@@ -342,6 +341,7 @@ class ExportResults:
self.exported_album += other.exported_album
self.skipped_album += other.skipped_album
self.missing_album += other.missing_album
self.metadata_changed += other.metadata_changed
return self
@@ -371,12 +371,14 @@ class ExportResults:
+ f",exported_album={self.exported_album}"
+ f",skipped_album={self.skipped_album}"
+ f",missing_album={self.missing_album}"
+ f",metadata_changed={self.metadata_changed}"
+ ")"
)
class PhotoExporter:
"""Export a photo"""
def __init__(self, photo: "PhotoInfo", tmpdir: t.Optional[str] = None):
self.photo = photo
self._render_options = RenderOptions()
@@ -739,7 +741,7 @@ class PhotoExporter:
return ShouldUpdate.EDITED_SIG_DIFFERENT
if options.force_update:
current_digest = hexdigest(self.photo.json())
current_digest = self.photo.hexdigest
if current_digest != file_record.digest:
# metadata in Photos changed, force update
return ShouldUpdate.DIGEST_DIFFERENT
@@ -1179,8 +1181,9 @@ class PhotoExporter:
rec.dest_sig = fileutil.file_sig(dest)
if options.exiftool:
rec.exifdata = self._exiftool_json_sidecar(options)
if options.force_update:
rec.digest = hexdigest(photoinfo)
if self.photo.hexdigest != rec.digest:
results.metadata_changed = [dest_str]
rec.digest = self.photo.hexdigest
return results
@@ -2011,13 +2014,6 @@ class PhotoExporter:
f.close()
def hexdigest(strval):
"""hexdigest of a string, using blake2b"""
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()
def _check_export_suffix(src, dest, edited):
"""Helper function for exporting photos to check file extensions of destination path.

View File

@@ -11,6 +11,7 @@ import os
import os.path
import pathlib
from datetime import timedelta, timezone
from functools import cached_property
from typing import Optional
import yaml
@@ -54,7 +55,7 @@ from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo
from .text_detection import detect_text
from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, list_directory
from .utils import _get_resource_loc, hexdigest, list_directory
__all__ = ["PhotoInfo", "PhotoInfoNone"]
@@ -892,38 +893,37 @@ class PhotoInfo:
return photopath
@property
@cached_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)"""
try:
return self._path_derivatives
except AttributeError:
if self._db._db_version <= _PHOTOS_4_VERSION:
self._path_derivatives = self._path_derivatives_4()
return self._path_derivatives
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)
derivatives = [
str(filename) for filename in files if filename.suffix != ".THM"
]
if (
self.isphoto
and len(derivatives) > 1
and derivatives[0].endswith(".mov")
):
derivatives[1], derivatives[0] = derivatives[0], derivatives[1]
if self.shared:
return self._path_derivatives_5_shared()
self._path_derivatives = derivatives
return self._path_derivatives
directory = self._uuid[0] # first char of uuid
derivative_path = (
pathlib.Path(self._db._library_path) / f"resources/derivatives/{directory}"
)
files = list(derivative_path.glob(f"{self.uuid}*.*"))
# previews may be missing from derivatives path
# there are what appear to be low res thumbnails in the "masters" subfolder
thumb_path = (
pathlib.Path(self._db._library_path)
/ f"resources/derivatives/masters/{directory}/{self.uuid}_4_5005_c.jpeg"
)
if thumb_path.exists():
files.append(thumb_path)
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)
derivatives = [str(filename) for filename in files if filename.suffix != ".THM"]
if self.isphoto and len(derivatives) > 1 and derivatives[0].endswith(".mov"):
derivatives[1], derivatives[0] = derivatives[0], derivatives[1]
return derivatives
def _path_derivatives_4(self):
"""Return paths to all derivative (preview) files for Photos <= 4"""
@@ -933,10 +933,7 @@ class PhotoInfo:
folder_id, file_id = _get_resource_loc(modelid)
derivatives_root = (
pathlib.Path(self._db._library_path)
/ "resources"
/ "proxies"
/ "derivatives"
/ folder_id
/ f"resources/proxies/derivatives/{folder_id}"
)
# photos appears to usually be in "00" subfolder but
@@ -961,6 +958,19 @@ class PhotoInfo:
# didn't find a derivatives path
return []
def _path_derivatives_5_shared(self):
"""Return paths to all derivative (preview) files for shared iCloud photos in Photos >= 5"""
directory = self._uuid[0] # first char of uuid
# only 1 derivative for shared photos and it's called 'UUID_4_5005_c.jpeg'
derivative_path = (
pathlib.Path(self._db._library_path)
/ "resources/cloudsharing/resources/derivatives/masters"
/ f"{directory}/{self.uuid}_4_5005_c.jpeg"
)
if derivative_path.exists():
return [str(derivative_path)]
return []
@property
def panorama(self):
"""Returns True if photo is a panorama, otherwise False"""
@@ -1356,6 +1366,12 @@ class PhotoInfo:
self._exiftool = exiftool
return self._exiftool
@cached_property
def hexdigest(self):
"""Returns a unique digest of the photo's properties and metadata;
useful for detecting changes in any property/metadata of the photo"""
return hexdigest(self.json())
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)

View File

@@ -3076,6 +3076,8 @@ class PhotosDB:
photos = _get_photos_by_attribute(
photos, "keywords", keyword, options.ignore_case
)
elif options.no_keyword:
photos = [p for p in photos if not p.keywords]
if person:
photos = _get_photos_by_attribute(

View File

@@ -56,6 +56,7 @@ class QueryOptions:
no_description: search for photos with no description
no_likes: search for shared photos with no likes
no_location: search for photos with no location
no_keyword: search for photos with no keywords
no_place: search for photos with no place
no_title: search for photos with no title
not_burst: search for non-burst photos
@@ -136,6 +137,7 @@ class QueryOptions:
no_description: Optional[bool] = None
no_likes: Optional[bool] = None
no_location: Optional[bool] = None
no_keyword: Optional[bool] = None
no_place: Optional[bool] = None
no_title: Optional[bool] = None
not_burst: Optional[bool] = None

View File

@@ -3,6 +3,7 @@
import datetime
import fnmatch
import glob
import hashlib
import importlib
import inspect
import logging
@@ -29,6 +30,7 @@ __all__ = [
"expand_and_validate_filepath",
"get_last_library_path",
"get_system_library_path",
"hexdigest",
"increment_filename_with_count",
"increment_filename",
"lineno",
@@ -514,6 +516,13 @@ def get_latest_version() -> Tuple[Optional[str], str]:
return None, e
def pluralize(count, singular, plural):
def pluralize(count: Optional[int], singular: str, plural: str) -> str:
"""Return singular or plural based on count"""
return singular if count == 1 else plural
def hexdigest(strval: str) -> str:
"""hexdigest of a string, using blake2b"""
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()

View File

@@ -12,8 +12,12 @@ from osxphotos.exiftool import _ExifToolProc
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
# run timewarp tests (configured with --timewarp)
TEST_TIMEWARP = False
# don't clean up crash logs (configured with --no-cleanup)
NO_CLEANUP = False
def get_os_version():
import platform
@@ -68,6 +72,12 @@ def pytest_addoption(parser):
parser.addoption(
"--timewarp", action="store_true", default=False, help="run --timewarp tests"
)
parser.addoption(
"--no-cleanup",
action="store_true",
default=False,
help="don't clean up crash logs after tests",
)
def pytest_configure(config):
@@ -81,10 +91,15 @@ def pytest_configure(config):
"markers", "timewarp: mark test as requiring --timewarp to run"
)
# this is hacky but I can't figure out how to check config options in other fixtures
if config.getoption("--timewarp"):
global TEST_TIMEWARP
TEST_TIMEWARP = True
if config.getoption("--no-cleanup"):
global NO_CLEANUP
NO_CLEANUP = True
def pytest_collection_modifyitems(config, items):
if not (config.getoption("--addalbum") and TEST_LIBRARY is not None):
@@ -163,7 +178,7 @@ def delete_crash_logs():
"""Delete left over crash logs from tests that were supposed to crash"""
yield
path = pathlib.Path(os.getcwd()) / "osxphotos_crash.log"
if path.is_file():
if path.is_file() and not NO_CLEANUP:
path.unlink()

View File

@@ -519,6 +519,7 @@ def test_path_derivatives(photosdb):
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_4_5005_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])

View File

@@ -625,6 +625,7 @@ def test_path_derivatives(photosdb):
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_4_5005_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])

View File

@@ -2711,6 +2711,27 @@ def test_query_keyword_4():
assert len(json_got) == 6
def test_query_no_keyword():
"""Test query --no-keyword"""
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--no-keyword",
"--added-before",
"2022-05-05",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 11
def test_query_person_1():
"""Test query --person"""
@@ -7715,3 +7736,80 @@ def test_export_added_in_last():
)
assert result.exit_code == 0
assert "Exporting" in result.output
def test_export_limit():
"""test export --limit"""
# Use --added-before so test doesn't break if photos added in the future
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--update",
"--limit",
"20",
"--added-before",
"2022-05-07",
],
)
assert result.exit_code == 0
assert "limit: 20/20 exported" in result.output
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--update",
"--limit",
"20",
"--added-before",
"2022-05-07",
],
)
assert result.exit_code == 0
assert "limit: 5/20 exported" in result.output
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--update",
"--limit",
"20",
"--added-before",
"2022-05-07",
],
)
assert result.exit_code == 0
assert "limit: 0/20 exported" in result.output
def test_export_no_keyword():
"""test export --no-keyword"""
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--no-keyword",
"--added-before",
"2022-05-05",
],
)
assert result.exit_code == 0
assert "Exporting 11" in result.output

View File

@@ -0,0 +1,33 @@
# Test cloud photos
import pytest
import osxphotos
PHOTOS_DB_CLOUD = "tests/Test-Cloud-10.16.0.photoslibrary"
UUID_DICT = {
"incloud": "FC638F58-84BE-4083-B5DE-F85BDC729062",
"shared": "2094984A-21DC-4A6E-88A6-7344F648B92E",
"cloudasset": "FC638F58-84BE-4083-B5DE-F85BDC729062",
}
@pytest.fixture(scope="module")
def photosdb():
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_CLOUD)
def test_incloud(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["incloud"]])
assert photos[0].incloud
def test_cloudasset_1(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["cloudasset"]])
assert photos[0].iscloudasset
def test_path_derivatives(photosdb):
photo = photosdb.get_photo(UUID_DICT["shared"])
assert photo.path_derivatives

View File

@@ -12,6 +12,7 @@ UUID_DICT = {
"not_incloud": "33000A44-E4BA-43A3-9304-62A0195AB4D9",
"cloudasset": "D11D25FF-5F31-47D2-ABA9-58418878DC15",
"not_cloudasset": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"shared": "4AD7C8EF-2991-4519-9D3A-7F44A6F031BE",
}
@@ -52,3 +53,8 @@ def test_cloudasset_3():
photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]])
assert not photos[0].iscloudasset
def test_path_derivatives(photosdb):
photo = photosdb.get_photo(UUID_DICT["shared"])
assert photo.path_derivatives

View File

@@ -524,6 +524,7 @@ def test_path_derivatives(photosdb):
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_4_5005_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])

View File

@@ -579,6 +579,7 @@ def test_path_derivatives(photosdb):
derivs = [
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_100_o.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_1_105_c.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068_4_5005_c.jpeg",
]
for i, p in enumerate(path):
assert p.endswith(derivs[i])