Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b09323b9fb | ||
|
|
213d84e964 | ||
|
|
6c57fb2df9 | ||
|
|
9c0b910046 | ||
|
|
1f40161950 | ||
|
|
d1aa4e92bd | ||
|
|
e7a17a8635 | ||
|
|
68754273de | ||
|
|
d28a2fe9bb | ||
|
|
382d097285 | ||
|
|
93de53da51 | ||
|
|
e272e95a85 | ||
|
|
84a96bd4d0 | ||
|
|
d26b625d57 | ||
|
|
8731e7d5bc | ||
|
|
2e501e6a9b | ||
|
|
7f4c981abe | ||
|
|
bbcc3acba9 | ||
|
|
fccd746c58 | ||
|
|
adb90a3364 | ||
|
|
445010e7e5 | ||
|
|
1227465aa7 | ||
|
|
de1900f10a | ||
|
|
ed315fffd2 | ||
|
|
be1f3a98d9 | ||
|
|
d8802368fc | ||
|
|
f132e9a843 | ||
|
|
6b342a1733 | ||
|
|
9dec028448 | ||
|
|
8be6a98c32 | ||
|
|
ce73c9cab8 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -32,4 +32,4 @@ jobs:
|
|||||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
python -m pytest tests/
|
python -m pytest -v tests/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ cli.spec
|
|||||||
docsrc/_build/
|
docsrc/_build/
|
||||||
venv/
|
venv/
|
||||||
.python-version
|
.python-version
|
||||||
|
cov.xml
|
||||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -4,6 +4,51 @@ 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).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.47.6](https://github.com/RhetTbull/osxphotos/compare/v0.47.5...v0.47.6)
|
||||||
|
|
||||||
|
> 27 March 2022
|
||||||
|
|
||||||
|
- fix verbose output when redirected to file, #661 [`382d097`](https://github.com/RhetTbull/osxphotos/commit/382d097285519e274bdb0cd16d6805152aa4a918)
|
||||||
|
- Updated docs [skip ci] [`6875427`](https://github.com/RhetTbull/osxphotos/commit/68754273de33ac9ed767fd43edcfe8b9c92e3a56)
|
||||||
|
- version bump [`d28a2fe`](https://github.com/RhetTbull/osxphotos/commit/d28a2fe9bb69034c19446c3dc70555b6043a43a0)
|
||||||
|
|
||||||
|
#### [v0.47.5](https://github.com/RhetTbull/osxphotos/compare/v0.47.4...v0.47.5)
|
||||||
|
|
||||||
|
> 12 March 2022
|
||||||
|
|
||||||
|
- Richify [`#653`](https://github.com/RhetTbull/osxphotos/pull/653)
|
||||||
|
- Added --watch, --breakpoint [`#652`](https://github.com/RhetTbull/osxphotos/pull/652)
|
||||||
|
- Hack to fix #654 when utime fails on NAS [`#654`](https://github.com/RhetTbull/osxphotos/issues/654)
|
||||||
|
- Debug updates [`de1900f`](https://github.com/RhetTbull/osxphotos/commit/de1900f10aaac8d703ef5d850a64f18dd5e01d40)
|
||||||
|
- Updated docs [skip ci] [`fccd746`](https://github.com/RhetTbull/osxphotos/commit/fccd746c581a319a8d1d5063133cbb9d5a4e1778)
|
||||||
|
- Changed return val of _should_update_photo to enum for easier debugging [`bbcc3ac`](https://github.com/RhetTbull/osxphotos/commit/bbcc3acba9a4dd3a9ebcc8136782005d35e46255)
|
||||||
|
- Updated crash_reporter to include crash data [`1227465`](https://github.com/RhetTbull/osxphotos/commit/1227465aa7e1d5a2fdaba4fc45cf917728d31170)
|
||||||
|
- Fixed missing pdb.py issue for pyinstaller, partial for #659 [`e272e95`](https://github.com/RhetTbull/osxphotos/commit/e272e95a856e5b448eb6ac85818757b974dfd6d4)
|
||||||
|
|
||||||
|
#### [v0.47.4](https://github.com/RhetTbull/osxphotos/compare/v0.47.3...v0.47.4)
|
||||||
|
|
||||||
|
> 2 March 2022
|
||||||
|
|
||||||
|
- Added --tmpdir, #650 [`#651`](https://github.com/RhetTbull/osxphotos/pull/651)
|
||||||
|
- Version bump [`6b342a1`](https://github.com/RhetTbull/osxphotos/commit/6b342a1733fa66d1663de2e3a234970f427a679f)
|
||||||
|
- Version bump [`f132e9a`](https://github.com/RhetTbull/osxphotos/commit/f132e9a8438023e4c69c7fb767d24dea7465db4d)
|
||||||
|
|
||||||
|
#### [v0.47.3](https://github.com/RhetTbull/osxphotos/compare/v0.47.2...v0.47.3)
|
||||||
|
|
||||||
|
> 27 February 2022
|
||||||
|
|
||||||
|
- Help topic [`#644`](https://github.com/RhetTbull/osxphotos/pull/644)
|
||||||
|
- updated docs [skip ci] [`ce73c9c`](https://github.com/RhetTbull/osxphotos/commit/ce73c9cab81fdd223dd49f2ff38608d553198412)
|
||||||
|
- Added -v to pytest [`8be6a98`](https://github.com/RhetTbull/osxphotos/commit/8be6a98c3208b5da0fa620d1884a17275ab56599)
|
||||||
|
|
||||||
|
#### [v0.47.2](https://github.com/RhetTbull/osxphotos/compare/v0.47.1...v0.47.2)
|
||||||
|
|
||||||
|
> 27 February 2022
|
||||||
|
|
||||||
|
- Updated README.md [`b275280`](https://github.com/RhetTbull/osxphotos/commit/b275280a1f3e1b1a61dcc95aefebd4e326a47377)
|
||||||
|
- Updated docs [skip ci] [`c95f682`](https://github.com/RhetTbull/osxphotos/commit/c95f682ca647f9b9e0718da658bdf7c735571f84)
|
||||||
|
- Fix for --load-config, #643 [`feb9538`](https://github.com/RhetTbull/osxphotos/commit/feb9538d1c5ad6569232e5900393befb8ec1a57e)
|
||||||
|
|
||||||
#### [v0.47.1](https://github.com/RhetTbull/osxphotos/compare/v0.47.0...v0.47.1)
|
#### [v0.47.1](https://github.com/RhetTbull/osxphotos/compare/v0.47.0...v0.47.1)
|
||||||
|
|
||||||
> 26 February 2022
|
> 26 February 2022
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
build
|
build
|
||||||
m2r2
|
m2r2
|
||||||
pdbpp
|
pdbpp
|
||||||
pyinstaller==4.4
|
pyinstaller==4.10
|
||||||
pytest-mock
|
pytest-mock
|
||||||
pytest==7.0.1
|
pytest==7.0.1
|
||||||
Sphinx
|
Sphinx
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# 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.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: bc3dce8a14bcd1b0c8a34e4d16f0011f
|
config: 61bf98593db44b8d320314e5cbec33cf
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Overview: module code — osxphotos 0.47.1 documentation</title>
|
<title>Overview: module code — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
||||||
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo — osxphotos 0.46.4 documentation</title>
|
<title>osxphotos.photoinfo — osxphotos 0.47.5 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css" />
|
||||||
<script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
<script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||||
@@ -87,7 +87,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">.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">.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">.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">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">list_directory</span><span class="p">,</span> <span class="n">_debug</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="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>
|
<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>
|
||||||
|
|
||||||
@@ -1856,7 +1856,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.46.6 documentation</title>
|
<title>osxphotos.photosdb.photosdb — osxphotos 0.47.5 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
|
||||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -83,15 +83,16 @@
|
|||||||
<span class="kn">from</span> <span class="nn">.._version</span> <span class="kn">import</span> <span class="n">__version__</span>
|
<span class="kn">from</span> <span class="nn">.._version</span> <span class="kn">import</span> <span class="n">__version__</span>
|
||||||
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">FolderInfo</span><span class="p">,</span> <span class="n">ImportInfo</span><span class="p">,</span> <span class="n">ProjectInfo</span>
|
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">FolderInfo</span><span class="p">,</span> <span class="n">ImportInfo</span><span class="p">,</span> <span class="n">ProjectInfo</span>
|
||||||
<span class="kn">from</span> <span class="nn">..datetime_utils</span> <span class="kn">import</span> <span class="n">datetime_has_tz</span><span class="p">,</span> <span class="n">datetime_naive_to_local</span>
|
<span class="kn">from</span> <span class="nn">..datetime_utils</span> <span class="kn">import</span> <span class="n">datetime_has_tz</span><span class="p">,</span> <span class="n">datetime_naive_to_local</span>
|
||||||
|
<span class="kn">from</span> <span class="nn">..debug</span> <span class="kn">import</span> <span class="n">is_debug</span>
|
||||||
<span class="kn">from</span> <span class="nn">..fileutil</span> <span class="kn">import</span> <span class="n">FileUtil</span>
|
<span class="kn">from</span> <span class="nn">..fileutil</span> <span class="kn">import</span> <span class="n">FileUtil</span>
|
||||||
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">PersonInfo</span>
|
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">PersonInfo</span>
|
||||||
<span class="kn">from</span> <span class="nn">..photoinfo</span> <span class="kn">import</span> <span class="n">PhotoInfo</span>
|
<span class="kn">from</span> <span class="nn">..photoinfo</span> <span class="kn">import</span> <span class="n">PhotoInfo</span>
|
||||||
<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">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
|
||||||
<span class="kn">from</span> <span class="nn">..queryoptions</span> <span class="kn">import</span> <span class="n">QueryOptions</span>
|
<span class="kn">from</span> <span class="nn">..queryoptions</span> <span class="kn">import</span> <span class="n">QueryOptions</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">..utils</span> <span class="kn">import</span> <span class="p">(</span>
|
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="p">(</span>
|
||||||
<span class="n">_check_file_exists</span><span class="p">,</span>
|
<span class="n">_check_file_exists</span><span class="p">,</span>
|
||||||
<span class="n">_db_is_locked</span><span class="p">,</span>
|
<span class="n">_db_is_locked</span><span class="p">,</span>
|
||||||
<span class="n">_debug</span><span class="p">,</span>
|
|
||||||
<span class="n">_get_os_version</span><span class="p">,</span>
|
<span class="n">_get_os_version</span><span class="p">,</span>
|
||||||
<span class="n">_open_sql_file</span><span class="p">,</span>
|
<span class="n">_open_sql_file</span><span class="p">,</span>
|
||||||
<span class="n">get_last_library_path</span><span class="p">,</span>
|
<span class="n">get_last_library_path</span><span class="p">,</span>
|
||||||
@@ -123,13 +124,14 @@
|
|||||||
<span class="n">labels_normalized_as_dict</span><span class="p">,</span>
|
<span class="n">labels_normalized_as_dict</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</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">dbfile</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</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">dbfile</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||||
<span class="sd">"""Create a new PhotosDB object.</span>
|
<span class="sd">"""Create a new PhotosDB object.</span>
|
||||||
|
|
||||||
<span class="sd"> Args:</span>
|
<span class="sd"> Args:</span>
|
||||||
<span class="sd"> dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.</span>
|
<span class="sd"> dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.</span>
|
||||||
<span class="sd"> verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.</span>
|
<span class="sd"> verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.</span>
|
||||||
<span class="sd"> exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH</span>
|
<span class="sd"> exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH</span>
|
||||||
|
<span class="sd"> rich: use rich with verbose output</span>
|
||||||
|
|
||||||
<span class="sd"> Raises:</span>
|
<span class="sd"> Raises:</span>
|
||||||
<span class="sd"> FileNotFoundError if dbfile is not a valid Photos library.</span>
|
<span class="sd"> FileNotFoundError if dbfile is not a valid Photos library.</span>
|
||||||
@@ -152,6 +154,12 @@
|
|||||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"verbose must be callable"</span><span class="p">)</span>
|
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"verbose must be callable"</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="n">verbose</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="n">verbose</span>
|
||||||
|
|
||||||
|
<span class="c1"># define functions for adding markup</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_filepath</span> <span class="o">=</span> <span class="n">add_rich_markup_tag</span><span class="p">(</span><span class="s2">"filepath"</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="n">rich</span><span class="p">)</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_filename</span> <span class="o">=</span> <span class="n">add_rich_markup_tag</span><span class="p">(</span><span class="s2">"filename"</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="n">rich</span><span class="p">)</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">add_rich_markup_tag</span><span class="p">(</span><span class="s2">"uuid"</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="n">rich</span><span class="p">)</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_num</span> <span class="o">=</span> <span class="n">add_rich_markup_tag</span><span class="p">(</span><span class="s2">"num"</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="n">rich</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># enable beta features</span>
|
<span class="c1"># enable beta features</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_beta</span> <span class="o">=</span> <span class="kc">False</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_beta</span> <span class="o">=</span> <span class="kc">False</span>
|
||||||
|
|
||||||
@@ -297,7 +305,7 @@
|
|||||||
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
|
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">dbfile</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">dbfile</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||||
@@ -314,7 +322,7 @@
|
|||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">_check_file_exists</span><span class="p">(</span><span class="n">dbfile</span><span class="p">):</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">_check_file_exists</span><span class="p">(</span><span class="n">dbfile</span><span class="p">):</span>
|
||||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2"> does not exist"</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">)</span>
|
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2"> does not exist"</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># init database names</span>
|
<span class="c1"># init database names</span>
|
||||||
@@ -328,7 +336,7 @@
|
|||||||
<span class="c1"># or photosanalysisd</span>
|
<span class="c1"># or photosanalysisd</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">dbfile</span><span class="p">)</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">dbfile</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_filepath</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># if database is exclusively locked, make a copy of it and use the copy</span>
|
<span class="c1"># if database is exclusively locked, make a copy of it and use the copy</span>
|
||||||
<span class="c1"># Photos maintains an exclusive lock on the database file while Photos is open</span>
|
<span class="c1"># Photos maintains an exclusive lock on the database file while Photos is open</span>
|
||||||
@@ -348,13 +356,13 @@
|
|||||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2"> does not exist"</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">)</span>
|
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2"> does not exist"</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">)</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="n">dbfile</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="n">dbfile</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_filepath</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
<span class="c1"># if database is exclusively locked, make a copy of it and use the copy</span>
|
<span class="c1"># if database is exclusively locked, make a copy of it and use the copy</span>
|
||||||
<span class="k">if</span> <span class="n">_db_is_locked</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="p">):</span>
|
<span class="k">if</span> <span class="n">_db_is_locked</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="p">):</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database locked, creating temporary copy."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database locked, creating temporary copy."</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_copy_db_file</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="p">)</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_copy_db_file</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||||
<span class="sa">f</span><span class="s2">"_dbfile = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="si">}</span><span class="s2">, _dbfile_actual = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="si">}</span><span class="s2">"</span>
|
<span class="sa">f</span><span class="s2">"_dbfile = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="si">}</span><span class="s2">, _dbfile_actual = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="si">}</span><span class="s2">"</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
@@ -369,7 +377,7 @@
|
|||||||
<span class="n">masters_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">library_path</span><span class="p">,</span> <span class="s2">"originals"</span><span class="p">)</span>
|
<span class="n">masters_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">library_path</span><span class="p">,</span> <span class="s2">"originals"</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_masters_path</span> <span class="o">=</span> <span class="n">masters_path</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_masters_path</span> <span class="o">=</span> <span class="n">masters_path</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"library = </span><span class="si">{</span><span class="n">library_path</span><span class="si">}</span><span class="s2">, masters = </span><span class="si">{</span><span class="n">masters_path</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"library = </span><span class="si">{</span><span class="n">library_path</span><span class="si">}</span><span class="s2">, masters = </span><span class="si">{</span><span class="n">masters_path</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span> <span class="o"><=</span> <span class="nb">int</span><span class="p">(</span><span class="n">_PHOTOS_4_VERSION</span><span class="p">):</span>
|
<span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span> <span class="o"><=</span> <span class="nb">int</span><span class="p">(</span><span class="n">_PHOTOS_4_VERSION</span><span class="p">):</span>
|
||||||
@@ -625,7 +633,7 @@
|
|||||||
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error copying</span><span class="si">{</span><span class="n">fname</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_path</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
|
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error copying</span><span class="si">{</span><span class="n">fname</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_path</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
|
||||||
<span class="k">raise</span> <span class="ne">Exception</span>
|
<span class="k">raise</span> <span class="ne">Exception</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">dest_path</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">dest_path</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">return</span> <span class="n">dest_path</span>
|
<span class="k">return</span> <span class="n">dest_path</span>
|
||||||
@@ -652,7 +660,7 @@
|
|||||||
<span class="c1"># print("Error linking " + fname + " to " + dest_path, file=sys.stderr)</span>
|
<span class="c1"># print("Error linking " + fname + " to " + dest_path, file=sys.stderr)</span>
|
||||||
<span class="c1"># raise Exception</span>
|
<span class="c1"># raise Exception</span>
|
||||||
|
|
||||||
<span class="c1"># if _debug():</span>
|
<span class="c1"># if is_debug():</span>
|
||||||
<span class="c1"># logging.debug(dest_path)</span>
|
<span class="c1"># logging.debug(dest_path)</span>
|
||||||
|
|
||||||
<span class="c1"># return dest_path</span>
|
<span class="c1"># return dest_path</span>
|
||||||
@@ -663,7 +671,7 @@
|
|||||||
|
|
||||||
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing database."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing database."</span><span class="p">)</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="si">}</span><span class="s2">."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_num</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span><span class="si">}</span><span class="s2">."</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">4</span> <span class="c1"># only used in Photos 5+</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">4</span> <span class="c1"># only used in Photos 5+</span>
|
||||||
|
|
||||||
@@ -1112,7 +1120,7 @@
|
|||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="c1"># unknown</span>
|
<span class="c1"># unknown</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
|
||||||
@@ -1335,7 +1343,7 @@
|
|||||||
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">==</span> <span class="mi">2</span>
|
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">==</span> <span class="mi">2</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||||
<span class="sa">f</span><span class="s2">"WARNING: found more than one edit_resource_id for "</span>
|
<span class="sa">f</span><span class="s2">"WARNING: found more than one edit_resource_id for "</span>
|
||||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||||
@@ -1614,7 +1622,7 @@
|
|||||||
<span class="sd"> but it works so don't touch it.</span>
|
<span class="sd"> but it works so don't touch it.</span>
|
||||||
<span class="sd"> """</span>
|
<span class="sd"> """</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"_process_database5"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"_process_database5"</span><span class="p">)</span>
|
||||||
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database."</span><span class="p">)</span>
|
||||||
@@ -1623,7 +1631,9 @@
|
|||||||
<span class="c1"># some of the tables/columns have different names in different versions of Photos</span>
|
<span class="c1"># some of the tables/columns have different names in different versions of Photos</span>
|
||||||
<span class="n">photos_ver</span> <span class="o">=</span> <span class="n">get_db_model_version</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
|
<span class="n">photos_ver</span> <span class="o">=</span> <span class="n">get_db_model_version</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="n">photos_ver</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="n">photos_ver</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">photos_ver</span><span class="si">}</span><span class="s2">."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span>
|
||||||
|
<span class="sa">f</span><span class="s2">"Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_num</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_num</span><span class="p">(</span><span class="n">photos_ver</span><span class="p">)</span><span class="si">}</span><span class="s2">."</span>
|
||||||
|
<span class="p">)</span>
|
||||||
<span class="n">asset_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"ASSET"</span><span class="p">]</span>
|
<span class="n">asset_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"ASSET"</span><span class="p">]</span>
|
||||||
<span class="n">keyword_join</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"KEYWORD_JOIN"</span><span class="p">]</span>
|
<span class="n">keyword_join</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"KEYWORD_JOIN"</span><span class="p">]</span>
|
||||||
<span class="n">asset_album_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"ASSET_ALBUM_TABLE"</span><span class="p">]</span>
|
<span class="n">asset_album_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"ASSET_ALBUM_TABLE"</span><span class="p">]</span>
|
||||||
@@ -1636,7 +1646,7 @@
|
|||||||
<span class="n">hdr_type_column</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"HDR_TYPE"</span><span class="p">]</span>
|
<span class="n">hdr_type_column</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"HDR_TYPE"</span><span class="p">]</span>
|
||||||
|
|
||||||
<span class="c1"># Look for all combinations of persons and pictures</span>
|
<span class="c1"># Look for all combinations of persons and pictures</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Getting information about persons"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Getting information about persons"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># get info to associate persons with photos</span>
|
<span class="c1"># get info to associate persons with photos</span>
|
||||||
@@ -2045,7 +2055,7 @@
|
|||||||
<span class="k">elif</span> <span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
|
<span class="k">elif</span> <span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
|
||||||
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
|
||||||
@@ -2244,7 +2254,7 @@
|
|||||||
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"extendedDescription"</span><span class="p">]</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"extendedDescription"</span><span class="p">]</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||||
<span class="sa">f</span><span class="s2">"WARNING: found description </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span>
|
<span class="sa">f</span><span class="s2">"WARNING: found description </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
@@ -2263,7 +2273,7 @@
|
|||||||
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"adjustmentFormatID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"adjustmentFormatID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||||
<span class="sa">f</span><span class="s2">"WARNING: found adjustmentformatidentifier </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for uuid </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
<span class="sa">f</span><span class="s2">"WARNING: found adjustmentformatidentifier </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for uuid </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
@@ -3607,7 +3617,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
docs/_static/basic.css
vendored
3
docs/_static/basic.css
vendored
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* Sphinx stylesheet -- basic theme.
|
* Sphinx stylesheet -- basic theme.
|
||||||
*
|
*
|
||||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
||||||
* :license: BSD, see LICENSE for details.
|
* :license: BSD, see LICENSE for details.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -757,6 +757,7 @@ span.pre {
|
|||||||
-ms-hyphens: none;
|
-ms-hyphens: none;
|
||||||
-webkit-hyphens: none;
|
-webkit-hyphens: none;
|
||||||
hyphens: none;
|
hyphens: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[class*="highlight-"] {
|
div[class*="highlight-"] {
|
||||||
|
|||||||
5
docs/_static/doctools.js
vendored
5
docs/_static/doctools.js
vendored
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* Sphinx JavaScript utilities for all documentation.
|
* Sphinx JavaScript utilities for all documentation.
|
||||||
*
|
*
|
||||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
||||||
* :license: BSD, see LICENSE for details.
|
* :license: BSD, see LICENSE for details.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -264,6 +264,9 @@ var Documentation = {
|
|||||||
hideSearchWords : function() {
|
hideSearchWords : function() {
|
||||||
$('#searchbox .highlight-link').fadeOut(300);
|
$('#searchbox .highlight-link').fadeOut(300);
|
||||||
$('span.highlighted').removeClass('highlighted');
|
$('span.highlighted').removeClass('highlighted');
|
||||||
|
var url = new URL(window.location);
|
||||||
|
url.searchParams.delete('highlight');
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.47.1',
|
VERSION: '0.47.7',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
2
docs/_static/language_data.js
vendored
2
docs/_static/language_data.js
vendored
@@ -5,7 +5,7 @@
|
|||||||
* This script contains the language-specific data used by searchtools.js,
|
* This script contains the language-specific data used by searchtools.js,
|
||||||
* namely the list of stopwords, stemmer, scorer and splitter.
|
* namely the list of stopwords, stemmer, scorer and splitter.
|
||||||
*
|
*
|
||||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
||||||
* :license: BSD, see LICENSE for details.
|
* :license: BSD, see LICENSE for details.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|||||||
2
docs/_static/searchtools.js
vendored
2
docs/_static/searchtools.js
vendored
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* Sphinx JavaScript utilities for the full-text search.
|
* Sphinx JavaScript utilities for the full-text search.
|
||||||
*
|
*
|
||||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
||||||
* :license: BSD, see LICENSE for details.
|
* :license: BSD, see LICENSE for details.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos command line interface (CLI) — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos command line interface (CLI) — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — osxphotos 0.47.1 documentation</title>
|
<title>Index — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -528,7 +528,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.47.1 documentation</title>
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -355,7 +355,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos package — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos package — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<h2>osxphotos module<a class="headerlink" href="#osxphotos-module" title="Permalink to this headline">¶</a></h2>
|
<h2>osxphotos module<a class="headerlink" href="#osxphotos-module" title="Permalink to this headline">¶</a></h2>
|
||||||
<dl class="py class">
|
<dl class="py class">
|
||||||
<dt class="sig sig-object py" id="osxphotos.PhotosDB">
|
<dt class="sig sig-object py" id="osxphotos.PhotosDB">
|
||||||
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">PhotosDB</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">dbfile</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">verbose</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">exiftool</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB" title="Permalink to this definition">¶</a></dt>
|
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">PhotosDB</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">dbfile</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">verbose</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">exiftool</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">rich</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB" title="Permalink to this definition">¶</a></dt>
|
||||||
<dd><p>Processes a Photos.app library database to extract information about photos</p>
|
<dd><p>Processes a Photos.app library database to extract information about photos</p>
|
||||||
<dl class="py property">
|
<dl class="py property">
|
||||||
<dt class="sig sig-object py" id="osxphotos.PhotosDB.album_info">
|
<dt class="sig sig-object py" id="osxphotos.PhotosDB.album_info">
|
||||||
@@ -975,7 +975,7 @@ Returns None if no associated RAW image</p>
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.47.1 documentation</title>
|
<title>Search — osxphotos 0.47.7 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,12 @@
|
|||||||
""" Example function for use with osxphotos export --post-function option """
|
""" Example function for use with osxphotos export --post-function option """
|
||||||
|
|
||||||
from osxphotos import PhotoInfo, ExportResults
|
from typing import Callable
|
||||||
|
|
||||||
|
from osxphotos import ExportResults, PhotoInfo
|
||||||
|
|
||||||
|
|
||||||
def post_function(
|
def post_function(
|
||||||
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
|
photo: PhotoInfo, results: ExportResults, verbose: Callable, **kwargs
|
||||||
):
|
):
|
||||||
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
|
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
|
||||||
This will get called immediately after the photo has been exported
|
This will get called immediately after the photo has been exported
|
||||||
|
|||||||
@@ -2,20 +2,27 @@
|
|||||||
# spec file for pyinstaller
|
# spec file for pyinstaller
|
||||||
# run `pyinstaller osxphotos.spec`
|
# run `pyinstaller osxphotos.spec`
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
pathex = os.getcwd()
|
pathex = os.getcwd()
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files
|
||||||
|
|
||||||
# include necessary data files
|
# include necessary data files
|
||||||
datas = [
|
datas = collect_data_files("osxphotos")
|
||||||
("osxphotos/templates/xmp_sidecar.mako", "osxphotos/templates"),
|
datas.extend(
|
||||||
("osxphotos/templates/xmp_sidecar_beta.mako", "osxphotos/templates"),
|
[
|
||||||
("osxphotos/phototemplate.tx", "osxphotos"),
|
("osxphotos/templates/xmp_sidecar.mako", "osxphotos/templates"),
|
||||||
("osxphotos/phototemplate.md", "osxphotos"),
|
("osxphotos/templates/xmp_sidecar_beta.mako", "osxphotos/templates"),
|
||||||
("osxphotos/tutorial.md", "osxphotos"),
|
("osxphotos/phototemplate.tx", "osxphotos"),
|
||||||
("osxphotos/exiftool_filetypes.json", "osxphotos"),
|
("osxphotos/phototemplate.md", "osxphotos"),
|
||||||
]
|
("osxphotos/tutorial.md", "osxphotos"),
|
||||||
|
("osxphotos/exiftool_filetypes.json", "osxphotos"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
||||||
for package, files in package_imports:
|
for package, files in package_imports:
|
||||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from ._constants import AlbumSortOrder
|
from ._constants import AlbumSortOrder
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
from .debug import is_debug, set_debug
|
||||||
from .exiftool import ExifTool
|
from .exiftool import ExifTool
|
||||||
from .export_db import ExportDB
|
from .export_db import ExportDB
|
||||||
from .fileutil import FileUtil, FileUtilNoOp
|
from .fileutil import FileUtil, FileUtilNoOp
|
||||||
@@ -14,13 +17,14 @@ from .placeinfo import PlaceInfo
|
|||||||
from .queryoptions import QueryOptions
|
from .queryoptions import QueryOptions
|
||||||
from .scoreinfo import ScoreInfo
|
from .scoreinfo import ScoreInfo
|
||||||
from .searchinfo import SearchInfo
|
from .searchinfo import SearchInfo
|
||||||
from .utils import _debug, _get_logger, _set_debug
|
from .utils import _get_logger
|
||||||
|
|
||||||
|
if not is_debug():
|
||||||
|
logging.disable(logging.DEBUG)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
"_debug",
|
|
||||||
"_get_logger",
|
"_get_logger",
|
||||||
"_set_debug",
|
|
||||||
"AlbumSortOrder",
|
"AlbumSortOrder",
|
||||||
"CommentInfo",
|
"CommentInfo",
|
||||||
"ExifTool",
|
"ExifTool",
|
||||||
@@ -30,6 +34,7 @@ __all__ = [
|
|||||||
"ExportResults",
|
"ExportResults",
|
||||||
"FileUtil",
|
"FileUtil",
|
||||||
"FileUtilNoOp",
|
"FileUtilNoOp",
|
||||||
|
"is_debug",
|
||||||
"LikeInfo",
|
"LikeInfo",
|
||||||
"MomentInfo",
|
"MomentInfo",
|
||||||
"PersonInfo",
|
"PersonInfo",
|
||||||
@@ -41,4 +46,5 @@ __all__ = [
|
|||||||
"QueryOptions",
|
"QueryOptions",
|
||||||
"ScoreInfo",
|
"ScoreInfo",
|
||||||
"SearchInfo",
|
"SearchInfo",
|
||||||
|
"set_debug",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import os.path
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
APP_NAME = "osxphotos"
|
||||||
|
|
||||||
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
||||||
|
|
||||||
# Time delta: add this to Photos times to get unix time
|
# Time delta: add this to Photos times to get unix time
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.47.2"
|
__version__ = "0.47.7"
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
"""cli package for osxphotos"""
|
"""cli package for osxphotos"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from rich import print
|
||||||
from rich.traceback import install as install_traceback
|
from rich.traceback import install as install_traceback
|
||||||
|
|
||||||
|
from osxphotos.debug import (
|
||||||
|
debug_breakpoint,
|
||||||
|
debug_watch,
|
||||||
|
get_debug_flags,
|
||||||
|
get_debug_options,
|
||||||
|
set_debug,
|
||||||
|
wrap_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
# apply any debug functions
|
||||||
|
# need to do this before importing anything else so that the debug functions
|
||||||
|
# wrap the right function references
|
||||||
|
# if a module does something like "from exiftool import ExifTool" and the user tries
|
||||||
|
# to wrap 'osxphotos.exiftool.ExifTool.asdict', the original ExifTool.asdict will be
|
||||||
|
# wrapped but the caller will have a reference to the function before it was wrapped
|
||||||
|
# reference: https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/13-ordering-issues-when-monkey-patching-in-python.md
|
||||||
|
args = get_debug_options(["--watch", "--breakpoint"], sys.argv)
|
||||||
|
for func_name in args.get("--watch", []):
|
||||||
|
try:
|
||||||
|
wrap_function(func_name, debug_watch)
|
||||||
|
print(f"Watching {func_name}")
|
||||||
|
except AttributeError:
|
||||||
|
print(f"{func_name} does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for func_name in args.get("--breakpoint", []):
|
||||||
|
try:
|
||||||
|
wrap_function(func_name, debug_breakpoint)
|
||||||
|
print(f"Breakpoint added for {func_name}")
|
||||||
|
except AttributeError:
|
||||||
|
print(f"{func_name} does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
args = get_debug_flags(["--debug"], sys.argv)
|
||||||
|
if args.get("--debug", False):
|
||||||
|
set_debug(True)
|
||||||
|
print("Debugging enabled")
|
||||||
|
|
||||||
from .about import about
|
from .about import about
|
||||||
from .albums import albums
|
from .albums import albums
|
||||||
from .cli import cli_main
|
from .cli import cli_main
|
||||||
from .common import get_photos_db, load_uuid_from_file, set_debug
|
from .common import get_photos_db, load_uuid_from_file
|
||||||
from .debug_dump import debug_dump
|
from .debug_dump import debug_dump
|
||||||
from .dump import dump
|
from .dump import dump
|
||||||
from .export import export
|
from .export import export
|
||||||
@@ -50,6 +90,7 @@ __all__ = [
|
|||||||
"query",
|
"query",
|
||||||
"repl",
|
"repl",
|
||||||
"run",
|
"run",
|
||||||
|
"set_debug",
|
||||||
"snap",
|
"snap",
|
||||||
"tutorial",
|
"tutorial",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from .places import places
|
|||||||
from .query import query
|
from .query import query
|
||||||
from .repl import repl
|
from .repl import repl
|
||||||
from .snap_diff import diff, snap
|
from .snap_diff import diff, snap
|
||||||
|
from .theme import theme
|
||||||
from .tutorial import tutorial
|
from .tutorial import tutorial
|
||||||
from .uuid import uuid
|
from .uuid import uuid
|
||||||
|
|
||||||
@@ -31,8 +32,6 @@ from .uuid import uuid
|
|||||||
# Click CLI object & context settings
|
# Click CLI object & context settings
|
||||||
class CLI_Obj:
|
class CLI_Obj:
|
||||||
def __init__(self, db=None, json=False, debug=False, group=None):
|
def __init__(self, db=None, json=False, debug=False, group=None):
|
||||||
if debug:
|
|
||||||
osxphotos._set_debug(True)
|
|
||||||
self.db = db
|
self.db = db
|
||||||
self.json = json
|
self.json = json
|
||||||
self.group = group
|
self.group = group
|
||||||
@@ -77,7 +76,9 @@ for command in [
|
|||||||
places,
|
places,
|
||||||
query,
|
query,
|
||||||
repl,
|
repl,
|
||||||
|
run,
|
||||||
snap,
|
snap,
|
||||||
|
theme,
|
||||||
tutorial,
|
tutorial,
|
||||||
uninstall,
|
uninstall,
|
||||||
uuid,
|
uuid,
|
||||||
|
|||||||
268
osxphotos/cli/click_rich_echo.py
Normal file
268
osxphotos/cli/click_rich_echo.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"""click.echo replacement that supports rich text formatting"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.theme import Theme
|
||||||
|
|
||||||
|
from .common import time_stamp
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_rich_console",
|
||||||
|
"get_rich_theme",
|
||||||
|
"rich_click_echo",
|
||||||
|
"rich_echo",
|
||||||
|
"rich_echo_error",
|
||||||
|
"rich_echo_via_pager",
|
||||||
|
"set_rich_console",
|
||||||
|
"set_rich_theme",
|
||||||
|
"set_rich_timestamp",
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO: this should really be a class instead of a module with a bunch of globals
|
||||||
|
|
||||||
|
# include emoji's in rich_echo_error output
|
||||||
|
ERROR_EMOJI = True
|
||||||
|
|
||||||
|
|
||||||
|
class _Console:
|
||||||
|
"""Store console object for rich output"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._console: t.Optional[Console] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def console(self):
|
||||||
|
return self._console
|
||||||
|
|
||||||
|
@console.setter
|
||||||
|
def console(self, console: Console):
|
||||||
|
self._console = console
|
||||||
|
|
||||||
|
|
||||||
|
_console = _Console()
|
||||||
|
|
||||||
|
_theme = None
|
||||||
|
|
||||||
|
_timestamp = False
|
||||||
|
|
||||||
|
# set to 1 if running tests
|
||||||
|
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
|
||||||
|
|
||||||
|
|
||||||
|
def set_rich_console(console: Console) -> None:
|
||||||
|
"""Set the console object to use for rich_echo and rich_echo_via_pager"""
|
||||||
|
global _console
|
||||||
|
_console.console = console
|
||||||
|
|
||||||
|
|
||||||
|
def get_rich_console() -> Console:
|
||||||
|
"""Get console object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Console object
|
||||||
|
"""
|
||||||
|
global _console
|
||||||
|
return _console.console
|
||||||
|
|
||||||
|
|
||||||
|
def set_rich_theme(theme: Theme) -> None:
|
||||||
|
"""Set the theme to use for rich_click_echo"""
|
||||||
|
global _theme
|
||||||
|
_theme = theme
|
||||||
|
|
||||||
|
|
||||||
|
def get_rich_theme() -> t.Optional[Theme]:
|
||||||
|
"""Get the theme to use for rich_click_echo"""
|
||||||
|
global _theme
|
||||||
|
return _theme
|
||||||
|
|
||||||
|
|
||||||
|
def set_rich_timestamp(timestamp: bool) -> None:
|
||||||
|
"""Set whether to print timestamp with rich_echo, rich_echo_error, and rich_click_error"""
|
||||||
|
global _timestamp
|
||||||
|
_timestamp = timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def rich_echo(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
theme=None,
|
||||||
|
markdown=False,
|
||||||
|
highlight=False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""Echo text to the console with rich formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The string or bytes to output. Other objects are converted to strings.
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
markdown: if True, interpret message as Markdown
|
||||||
|
highlight: if True, use automatic rich.print highlighting
|
||||||
|
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
|
||||||
|
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
|
||||||
|
all other values passed to rich.console.Console.print()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# args for click.echo that may have been passed in kwargs
|
||||||
|
echo_args = {}
|
||||||
|
for arg in ("file", "nl", "err", "color"):
|
||||||
|
val = kwargs.pop(arg, None)
|
||||||
|
if val is not None:
|
||||||
|
echo_args[arg] = val
|
||||||
|
|
||||||
|
width = kwargs.pop("width", None)
|
||||||
|
if width is None and OSXPHOTOS_IS_TESTING:
|
||||||
|
# if not outputting to terminal, use a huge width to avoid wrapping
|
||||||
|
# otherwise tests fail
|
||||||
|
width = 10_000
|
||||||
|
console = get_rich_console() or Console(theme=theme, width=width)
|
||||||
|
if markdown:
|
||||||
|
message = Markdown(message)
|
||||||
|
# Markdown always adds a new line so disable unless explicitly specified
|
||||||
|
global _timestamp
|
||||||
|
if _timestamp:
|
||||||
|
message = time_stamp() + message
|
||||||
|
console.print(message, highlight=highlight, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def rich_echo_error(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
theme=None,
|
||||||
|
markdown=False,
|
||||||
|
highlight=False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""Echo text to the console with rich formatting and if stdout is redirected, echo to stderr
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The string or bytes to output. Other objects are converted to strings.
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
markdown: if True, interpret message as Markdown
|
||||||
|
highlight: if True, use automatic rich.print highlighting
|
||||||
|
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
|
||||||
|
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
|
||||||
|
all other values passed to rich.console.Console.print()
|
||||||
|
"""
|
||||||
|
|
||||||
|
global ERROR_EMOJI
|
||||||
|
if ERROR_EMOJI:
|
||||||
|
if "[error]" in message:
|
||||||
|
message = f":cross_mark-emoji: {message}"
|
||||||
|
elif "[warning]" in message:
|
||||||
|
message = f":warning-emoji: {message}"
|
||||||
|
|
||||||
|
console = get_rich_console() or Console(theme=theme or get_rich_theme())
|
||||||
|
if not console.is_terminal:
|
||||||
|
# if stdout is redirected, echo to stderr
|
||||||
|
rich_click_echo(
|
||||||
|
message,
|
||||||
|
theme=theme or get_rich_theme(),
|
||||||
|
markdown=markdown,
|
||||||
|
highlight=highlight,
|
||||||
|
**kwargs,
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rich_echo(
|
||||||
|
message,
|
||||||
|
theme=theme or get_rich_theme(),
|
||||||
|
markdown=markdown,
|
||||||
|
highlight=highlight,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rich_click_echo(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
theme=None,
|
||||||
|
markdown=False,
|
||||||
|
highlight=False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""Echo text to the console with rich formatting using click.echo
|
||||||
|
|
||||||
|
This is a wrapper around click.echo that supports rich text formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The string or bytes to output. Other objects are converted to strings.
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
markdown: if True, interpret message as Markdown
|
||||||
|
highlight: if True, use automatic rich.print highlighting
|
||||||
|
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
|
||||||
|
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
|
||||||
|
all other values passed to rich.console.Console.print()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# args for click.echo that may have been passed in kwargs
|
||||||
|
echo_args = {}
|
||||||
|
for arg in ("file", "nl", "err", "color"):
|
||||||
|
val = kwargs.pop(arg, None)
|
||||||
|
if val is not None:
|
||||||
|
echo_args[arg] = val
|
||||||
|
|
||||||
|
# click.echo will include "\n" so don't add it here unless specified
|
||||||
|
end = kwargs.pop("end", "")
|
||||||
|
|
||||||
|
if width := kwargs.pop("width", None) is None:
|
||||||
|
# if not outputting to terminal, use a huge width to avoid wrapping
|
||||||
|
# otherwise tests fail
|
||||||
|
temp_console = Console()
|
||||||
|
width = temp_console.width if temp_console.is_terminal else 10_000
|
||||||
|
console = Console(
|
||||||
|
force_terminal=True,
|
||||||
|
theme=theme or get_rich_theme(),
|
||||||
|
width=width,
|
||||||
|
)
|
||||||
|
if markdown:
|
||||||
|
message = Markdown(message)
|
||||||
|
# Markdown always adds a new line so disable unless explicitly specified
|
||||||
|
echo_args["nl"] = echo_args.get("nl") is True
|
||||||
|
global _timestamp
|
||||||
|
if _timestamp:
|
||||||
|
message = time_stamp() + message
|
||||||
|
with console.capture() as capture:
|
||||||
|
console.print(message, end=end, highlight=highlight, **kwargs)
|
||||||
|
click.echo(capture.get(), **echo_args)
|
||||||
|
|
||||||
|
|
||||||
|
def rich_echo_via_pager(
|
||||||
|
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
|
||||||
|
theme: t.Optional[Theme] = None,
|
||||||
|
highlight=False,
|
||||||
|
markdown: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
"""This function takes a text and shows it via an environment specific
|
||||||
|
pager on stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text_or_generator: the text to page, or alternatively, a generator emitting the text to page.
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
markdown: if True, interpret message as Markdown
|
||||||
|
highlight: if True, use automatic rich.print highlighting
|
||||||
|
**kwargs: if "color" in kwargs, works the same as click.echo_via_pager(color=color)
|
||||||
|
otherwise any kwargs are passed to rich.Console.print()
|
||||||
|
"""
|
||||||
|
if inspect.isgeneratorfunction(text_or_generator):
|
||||||
|
text_or_generator = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
|
||||||
|
elif isinstance(text_or_generator, str):
|
||||||
|
text_or_generator = [text_or_generator]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
text_or_generator = iter(text_or_generator)
|
||||||
|
except TypeError:
|
||||||
|
text_or_generator = [text_or_generator]
|
||||||
|
|
||||||
|
console = _console.console or Console(theme=theme)
|
||||||
|
|
||||||
|
color = kwargs.pop("color", True)
|
||||||
|
|
||||||
|
with console.pager(styles=color):
|
||||||
|
for x in text_or_generator:
|
||||||
|
if isinstance(x, str) and markdown:
|
||||||
|
x = Markdown(x)
|
||||||
|
console.print(x, highlight=highlight, **kwargs)
|
||||||
194
osxphotos/cli/color_themes.py
Normal file
194
osxphotos/cli/color_themes.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"""Support for colorized output for osxphotos cli using rich"""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.style import Style
|
||||||
|
from rich_theme_manager import Theme, ThemeManager
|
||||||
|
|
||||||
|
from .common import get_config_dir, noop
|
||||||
|
from .darkmode import is_dark_mode
|
||||||
|
|
||||||
|
DEFAULT_THEME_NAME = "default"
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_default_theme",
|
||||||
|
"get_theme",
|
||||||
|
"get_theme_dir",
|
||||||
|
"get_theme_manager",
|
||||||
|
DEFAULT_THEME_NAME,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
THEME_STYLES = [
|
||||||
|
"color",
|
||||||
|
"count",
|
||||||
|
"error",
|
||||||
|
"filename",
|
||||||
|
"filepath",
|
||||||
|
"highlight",
|
||||||
|
"num",
|
||||||
|
"time",
|
||||||
|
"uuid",
|
||||||
|
"warning",
|
||||||
|
"bar.back",
|
||||||
|
"bar.complete",
|
||||||
|
"bar.finished",
|
||||||
|
"bar.pulse",
|
||||||
|
"progress.elapsed",
|
||||||
|
"progress.percentage",
|
||||||
|
"progress.remaining",
|
||||||
|
]
|
||||||
|
|
||||||
|
COLOR_THEMES = {
|
||||||
|
"dark": Theme(
|
||||||
|
name="dark",
|
||||||
|
description="Dark mode theme",
|
||||||
|
tags=["dark"],
|
||||||
|
styles={
|
||||||
|
# color pallette from https://github.com/dracula/dracula-theme
|
||||||
|
"color": Style(color="rgb(248,248,242)"),
|
||||||
|
"count": Style(color="rgb(139,233,253)"),
|
||||||
|
"error": Style(color="rgb(255,85,85)", bold=True),
|
||||||
|
"filename": Style(color="rgb(189,147,249)", bold=True),
|
||||||
|
"filepath": Style(color="rgb(80,250,123)", bold=True),
|
||||||
|
"highlight": Style(color="#000000", bgcolor="#d73a49", bold=True),
|
||||||
|
"num": Style(color="rgb(139,233,253)", bold=True),
|
||||||
|
"time": Style(color="rgb(139,233,253)", bold=True),
|
||||||
|
"uuid": Style(color="rgb(255,184,108)"),
|
||||||
|
"warning": Style(color="rgb(241,250,140)", bold=True),
|
||||||
|
"bar.back": Style(color="rgb(68,71,90)"),
|
||||||
|
"bar.complete": Style(color="rgb(249,38,114)"),
|
||||||
|
"bar.finished": Style(color="rgb(80,250,123)"),
|
||||||
|
"bar.pulse": Style(color="rgb(98,114,164)"),
|
||||||
|
"progress.elapsed": Style(color="rgb(139,233,253)"),
|
||||||
|
"progress.percentage": Style(color="rgb(255,121,198)"),
|
||||||
|
"progress.remaining": Style(color="rgb(139,233,253)"),
|
||||||
|
# "headers": Style(color="rgb(165,194,97)"),
|
||||||
|
# "options": Style(color="rgb(255,198,109)"),
|
||||||
|
# "metavar": Style(color="rgb(12,125,157)"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"light": Theme(
|
||||||
|
name="light",
|
||||||
|
description="Light mode theme",
|
||||||
|
styles={
|
||||||
|
"color": Style(color="#000000"),
|
||||||
|
"count": Style(color="#005cc5", bold=True),
|
||||||
|
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
|
||||||
|
"filename": Style(color="#6f42c1", bold=True),
|
||||||
|
"filepath": Style(color="#22863a", bold=True),
|
||||||
|
"highlight": Style(color="#ffffff", bgcolor="#d73a49", bold=True),
|
||||||
|
"num": Style(color="#005cc5", bold=True),
|
||||||
|
"time": Style(color="#032f62", bold=True),
|
||||||
|
"uuid": Style(color="#d73a49", bold=True),
|
||||||
|
"warning": Style(color="#e36209", bold=True, underline=True, italic=True),
|
||||||
|
"bar.back": Style(color="grey23"),
|
||||||
|
"bar.complete": Style(color="rgb(249,38,114)"),
|
||||||
|
"bar.finished": Style(color="rgb(114,156,31)"),
|
||||||
|
"bar.pulse": Style(color="rgb(249,38,114)"),
|
||||||
|
"progress.elapsed": Style(color="#032f62", bold=True),
|
||||||
|
"progress.percentage": Style(color="#6f42c1", bold=True),
|
||||||
|
"progress.remaining": Style(color="#032f62", bold=True),
|
||||||
|
# "headers": Style(color="rgb(254,212,66)"),
|
||||||
|
# "options": Style(color="rgb(227,98,9)"),
|
||||||
|
# "metavar": Style(color="rgb(111,66,193)"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"mono": Theme(
|
||||||
|
name="mono",
|
||||||
|
description="Monochromatic theme",
|
||||||
|
tags=["mono", "colorblind"],
|
||||||
|
styles={
|
||||||
|
"count": "bold",
|
||||||
|
"error": "reverse italic",
|
||||||
|
"filename": "bold",
|
||||||
|
"filepath": "bold underline",
|
||||||
|
"highlight": "reverse italic",
|
||||||
|
"num": "bold",
|
||||||
|
"time": "bold",
|
||||||
|
"uuid": "bold",
|
||||||
|
"warning": "bold italic",
|
||||||
|
"bar.back": "",
|
||||||
|
"bar.complete": "reverse",
|
||||||
|
"bar.finished": "bold",
|
||||||
|
"bar.pulse": "bold",
|
||||||
|
"progress.elapsed": "",
|
||||||
|
"progress.percentage": "bold",
|
||||||
|
"progress.remaining": "bold",
|
||||||
|
# "headers": "bold",
|
||||||
|
# "options": "bold",
|
||||||
|
# "metavar": "bold",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"plain": Theme(
|
||||||
|
name="plain",
|
||||||
|
description="Plain theme with no colors",
|
||||||
|
tags=["colorblind"],
|
||||||
|
styles={
|
||||||
|
"color": "",
|
||||||
|
"count": "",
|
||||||
|
"error": "",
|
||||||
|
"filename": "",
|
||||||
|
"filepath": "",
|
||||||
|
"highlight": "",
|
||||||
|
"num": "",
|
||||||
|
"time": "",
|
||||||
|
"uuid": "",
|
||||||
|
"warning": "",
|
||||||
|
"bar.back": "",
|
||||||
|
"bar.complete": "",
|
||||||
|
"bar.finished": "",
|
||||||
|
"bar.pulse": "",
|
||||||
|
"progress.elapsed": "",
|
||||||
|
"progress.percentage": "",
|
||||||
|
"progress.remaining": "",
|
||||||
|
# "headers": "",
|
||||||
|
# "options": "",
|
||||||
|
# "metavar": "",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_theme_dir() -> pathlib.Path:
|
||||||
|
"""Return the theme config dir, creating it if necessary"""
|
||||||
|
theme_dir = get_config_dir() / "themes"
|
||||||
|
if not theme_dir.exists():
|
||||||
|
theme_dir.mkdir()
|
||||||
|
return theme_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_theme_manager() -> ThemeManager:
|
||||||
|
"""Return theme manager instance"""
|
||||||
|
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
|
||||||
|
|
||||||
|
|
||||||
|
def get_theme(
|
||||||
|
theme_name: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Get theme by name, or default theme if no name is provided"""
|
||||||
|
|
||||||
|
if theme_name is None:
|
||||||
|
return get_default_theme()
|
||||||
|
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
try:
|
||||||
|
return theme_manager.get(theme_name)
|
||||||
|
except ValueError as e:
|
||||||
|
raise click.ClickException(
|
||||||
|
f"Theme '{theme_name}' not found. "
|
||||||
|
f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_theme():
|
||||||
|
"""Get the default color theme"""
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
try:
|
||||||
|
return theme_manager.get(DEFAULT_THEME_NAME)
|
||||||
|
except ValueError:
|
||||||
|
return (
|
||||||
|
theme_manager.get("dark") if is_dark_mode() else theme_manager.get("light")
|
||||||
|
)
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
"""Globals and constants use by the CLI commands"""
|
"""Globals and constants use by the CLI commands"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Callable
|
from datetime import datetime
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
from osxphotos._constants import APP_NAME
|
||||||
from osxphotos._version import __version__
|
from osxphotos._version import __version__
|
||||||
|
|
||||||
from .param_types import *
|
from .param_types import *
|
||||||
|
|
||||||
from rich import print as rprint
|
|
||||||
|
|
||||||
# global variable to control debug output
|
|
||||||
# set via --debug
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
# used to show/hide hidden commands
|
# used to show/hide hidden commands
|
||||||
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
||||||
|
|
||||||
@@ -25,21 +20,26 @@ OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
|||||||
OSXPHOTOS_SNAPSHOT_DIR = "/private/tmp/osxphotos_snapshots"
|
OSXPHOTOS_SNAPSHOT_DIR = "/private/tmp/osxphotos_snapshots"
|
||||||
|
|
||||||
# where to write the crash report if osxphotos crashes
|
# where to write the crash report if osxphotos crashes
|
||||||
OSXPHOTOS_CRASH_LOG = os.getcwd() + "/osxphotos_crash.log"
|
OSXPHOTOS_CRASH_LOG = f"{os.getcwd()}/osxphotos_crash.log"
|
||||||
|
|
||||||
CLI_COLOR_ERROR = "red"
|
CLI_COLOR_ERROR = "red"
|
||||||
CLI_COLOR_WARNING = "yellow"
|
CLI_COLOR_WARNING = "yellow"
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
def set_debug(debug: bool):
|
"CLI_COLOR_ERROR",
|
||||||
"""set debug flag"""
|
"CLI_COLOR_WARNING",
|
||||||
global DEBUG
|
"DB_ARGUMENT",
|
||||||
DEBUG = debug
|
"DB_OPTION",
|
||||||
|
"DEBUG_OPTIONS",
|
||||||
|
"DELETED_OPTIONS",
|
||||||
def is_debug():
|
"JSON_OPTION",
|
||||||
"""return debug flag"""
|
"QUERY_OPTIONS",
|
||||||
return DEBUG
|
"THEME_OPTION",
|
||||||
|
"get_photos_db",
|
||||||
|
"load_uuid_from_file",
|
||||||
|
"noop",
|
||||||
|
"time_stamp",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
@@ -47,50 +47,9 @@ def noop(*args, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def verbose_print(
|
def time_stamp() -> str:
|
||||||
verbose: bool = True, timestamp: bool = False, rich=False
|
"""return timestamp"""
|
||||||
) -> Callable:
|
return f"[time]{str(datetime.now())}[/time] -- "
|
||||||
"""Create verbose function to print output
|
|
||||||
|
|
||||||
Args:
|
|
||||||
verbose: if True, returns verbose print function otherwise returns no-op function
|
|
||||||
timestamp: if True, includes timestamp in verbose output
|
|
||||||
rich: use rich.print instead of click.echo
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
function to print output
|
|
||||||
"""
|
|
||||||
if not verbose:
|
|
||||||
return noop
|
|
||||||
|
|
||||||
# closure to capture timestamp
|
|
||||||
def verbose_(*args, **kwargs):
|
|
||||||
"""print output if verbose flag set"""
|
|
||||||
styled_args = []
|
|
||||||
timestamp_str = str(datetime.datetime.now()) + " -- " if timestamp else ""
|
|
||||||
for arg in args:
|
|
||||||
if type(arg) == str:
|
|
||||||
arg = timestamp_str + arg
|
|
||||||
if "error" in arg.lower():
|
|
||||||
arg = click.style(arg, fg=CLI_COLOR_ERROR)
|
|
||||||
elif "warning" in arg.lower():
|
|
||||||
arg = click.style(arg, fg=CLI_COLOR_WARNING)
|
|
||||||
styled_args.append(arg)
|
|
||||||
click.echo(*styled_args, **kwargs)
|
|
||||||
|
|
||||||
def rich_verbose_(*args, **kwargs):
|
|
||||||
"""print output if verbose flag set using rich.print"""
|
|
||||||
timestamp_str = str(datetime.datetime.now()) + " -- " if timestamp else ""
|
|
||||||
for arg in args:
|
|
||||||
if type(arg) == str:
|
|
||||||
arg = timestamp_str + arg
|
|
||||||
if "error" in arg.lower():
|
|
||||||
arg = f"[{CLI_COLOR_ERROR}]{arg}[/{CLI_COLOR_ERROR}]"
|
|
||||||
elif "warning" in arg.lower():
|
|
||||||
arg = f"[{CLI_COLOR_WARNING}]{arg}[/{CLI_COLOR_WARNING}]"
|
|
||||||
rprint(arg, **kwargs)
|
|
||||||
|
|
||||||
return rich_verbose_ if rich else verbose_
|
|
||||||
|
|
||||||
|
|
||||||
def get_photos_db(*db_options):
|
def get_photos_db(*db_options):
|
||||||
@@ -511,6 +470,47 @@ def QUERY_OPTIONS(f):
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def DEBUG_OPTIONS(f):
|
||||||
|
o = click.option
|
||||||
|
options = [
|
||||||
|
o(
|
||||||
|
"--debug",
|
||||||
|
is_flag=True,
|
||||||
|
help="Enable debug output.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
o(
|
||||||
|
"--watch",
|
||||||
|
metavar="FUNCTION_PATH",
|
||||||
|
multiple=True,
|
||||||
|
help="Watch function calls. For example, to watch all calls to FileUtil.copy: "
|
||||||
|
"'--watch osxphotos.fileutil.FileUtil.copy'. More than one --watch option can be specified.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
o(
|
||||||
|
"--breakpoint",
|
||||||
|
metavar="FUNCTION_PATH",
|
||||||
|
multiple=True,
|
||||||
|
help="Add breakpoint to function calls. For example, to add breakpoint to FileUtil.copy: "
|
||||||
|
"'--breakpoint osxphotos.fileutil.FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for o in options[::-1]:
|
||||||
|
f = o(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
THEME_OPTION = click.option(
|
||||||
|
"--theme",
|
||||||
|
metavar="THEME",
|
||||||
|
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||||
|
help="Specify the color theme to use for --verbose output. "
|
||||||
|
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||||
|
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_uuid_from_file(filename):
|
def load_uuid_from_file(filename):
|
||||||
"""Load UUIDs from file. Does not validate UUIDs.
|
"""Load UUIDs from file. Does not validate UUIDs.
|
||||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||||
@@ -536,3 +536,11 @@ def load_uuid_from_file(filename):
|
|||||||
if len(line) and line[0] != "#":
|
if len(line) and line[0] != "#":
|
||||||
uuid.append(line)
|
uuid.append(line)
|
||||||
return uuid
|
return uuid
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_dir() -> pathlib.Path:
|
||||||
|
"""Get the directory where config files are stored."""
|
||||||
|
config_dir = pathlib.Path.home() / ".config" / APP_NAME
|
||||||
|
if not config_dir.is_dir():
|
||||||
|
config_dir.mkdir(parents=True)
|
||||||
|
return config_dir
|
||||||
|
|||||||
19
osxphotos/cli/darkmode.py
Normal file
19
osxphotos/cli/darkmode.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"""Detect dark mode on MacOS >= 10.14"""
|
||||||
|
|
||||||
|
import objc
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
def theme():
|
||||||
|
with objc.autorelease_pool():
|
||||||
|
user_defaults = Foundation.NSUserDefaults.standardUserDefaults()
|
||||||
|
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
|
||||||
|
return "dark" if system_theme == "Dark" else "light"
|
||||||
|
|
||||||
|
|
||||||
|
def is_dark_mode():
|
||||||
|
return theme() == "dark"
|
||||||
|
|
||||||
|
|
||||||
|
def is_light_mode():
|
||||||
|
return theme() == "light"
|
||||||
@@ -9,15 +9,9 @@ from rich import print
|
|||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||||
|
|
||||||
from .common import (
|
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
|
||||||
DB_ARGUMENT,
|
|
||||||
DB_OPTION,
|
|
||||||
JSON_OPTION,
|
|
||||||
OSXPHOTOS_HIDDEN,
|
|
||||||
get_photos_db,
|
|
||||||
verbose_print,
|
|
||||||
)
|
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
|
from .verbose import verbose_print
|
||||||
|
|
||||||
|
|
||||||
@click.command(hidden=OSXPHOTOS_HIDDEN)
|
@click.command(hidden=OSXPHOTOS_HIDDEN)
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ from osxphotos.configoptions import (
|
|||||||
ConfigOptionsInvalidError,
|
ConfigOptionsInvalidError,
|
||||||
ConfigOptionsLoadError,
|
ConfigOptionsLoadError,
|
||||||
)
|
)
|
||||||
from osxphotos.crash_reporter import crash_reporter
|
from osxphotos.crash_reporter import crash_reporter, set_crash_data
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
from osxphotos.debug import is_debug, set_debug
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
||||||
from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||||
@@ -56,32 +57,45 @@ from osxphotos.queryoptions import QueryOptions
|
|||||||
from osxphotos.uti import get_preferred_uti_extension
|
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
|
||||||
|
|
||||||
|
from .click_rich_echo import (
|
||||||
|
rich_click_echo,
|
||||||
|
rich_echo,
|
||||||
|
rich_echo_error,
|
||||||
|
set_rich_console,
|
||||||
|
set_rich_theme,
|
||||||
|
set_rich_timestamp,
|
||||||
|
)
|
||||||
|
from .color_themes import get_theme
|
||||||
from .common import (
|
from .common import (
|
||||||
CLI_COLOR_ERROR,
|
CLI_COLOR_ERROR,
|
||||||
CLI_COLOR_WARNING,
|
CLI_COLOR_WARNING,
|
||||||
DB_ARGUMENT,
|
DB_ARGUMENT,
|
||||||
DB_OPTION,
|
DB_OPTION,
|
||||||
|
DEBUG_OPTIONS,
|
||||||
DELETED_OPTIONS,
|
DELETED_OPTIONS,
|
||||||
JSON_OPTION,
|
JSON_OPTION,
|
||||||
OSXPHOTOS_CRASH_LOG,
|
OSXPHOTOS_CRASH_LOG,
|
||||||
OSXPHOTOS_HIDDEN,
|
OSXPHOTOS_HIDDEN,
|
||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
|
THEME_OPTION,
|
||||||
get_photos_db,
|
get_photos_db,
|
||||||
is_debug,
|
|
||||||
load_uuid_from_file,
|
load_uuid_from_file,
|
||||||
noop,
|
noop,
|
||||||
set_debug,
|
|
||||||
verbose_print,
|
|
||||||
)
|
)
|
||||||
from .help import ExportCommand, get_help_msg
|
from .help import ExportCommand, get_help_msg
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
from .param_types import ExportDBType, FunctionCall
|
from .param_types import ExportDBType, FunctionCall
|
||||||
|
from .rich_progress import rich_progress
|
||||||
|
from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||||
|
|
||||||
|
|
||||||
@click.command(cls=ExportCommand)
|
@click.command(cls=ExportCommand)
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||||
|
@click.option(
|
||||||
|
"--no-progress", is_flag=True, help="Do not display progress bar during export."
|
||||||
|
)
|
||||||
@QUERY_OPTIONS
|
@QUERY_OPTIONS
|
||||||
@click.option(
|
@click.option(
|
||||||
"--missing",
|
"--missing",
|
||||||
@@ -562,6 +576,16 @@ from .param_types import ExportDBType, FunctionCall
|
|||||||
"may improve performance when exporting over a network or slow disk but could result in "
|
"may improve performance when exporting over a network or slow disk but could result in "
|
||||||
"losing update state information if the program is interrupted or crashes.",
|
"losing update state information if the program is interrupted or crashes.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--tmpdir",
|
||||||
|
metavar="DIR",
|
||||||
|
help="Specify alternate temporary directory. Default is system temporary directory. "
|
||||||
|
"osxphotos needs to create a number of temporary files during export. In some cases, "
|
||||||
|
"particularly if the Photos library is on an APFS volume that is not the system volume, "
|
||||||
|
"osxphotos may run faster if you specify a temporary directory on the same volume as "
|
||||||
|
"the Photos library.",
|
||||||
|
type=click.Path(dir_okay=True, file_okay=False, exists=True),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--load-config",
|
"--load-config",
|
||||||
required=False,
|
required=False,
|
||||||
@@ -619,14 +643,8 @@ from .param_types import ExportDBType, FunctionCall
|
|||||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||||
"Default = 'cumulative'.",
|
"Default = 'cumulative'.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@THEME_OPTION
|
||||||
"--debug",
|
@DEBUG_OPTIONS
|
||||||
required=False,
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
hidden=OSXPHOTOS_HIDDEN,
|
|
||||||
help="Enable debug output.",
|
|
||||||
)
|
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@@ -670,6 +688,7 @@ def export(
|
|||||||
to_time,
|
to_time,
|
||||||
verbose,
|
verbose,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
no_progress,
|
||||||
missing,
|
missing,
|
||||||
update,
|
update,
|
||||||
force_update,
|
force_update,
|
||||||
@@ -756,6 +775,7 @@ def export(
|
|||||||
add_missing_to_album,
|
add_missing_to_album,
|
||||||
exportdb,
|
exportdb,
|
||||||
ramdb,
|
ramdb,
|
||||||
|
tmpdir,
|
||||||
load_config,
|
load_config,
|
||||||
save_config,
|
save_config,
|
||||||
config_only,
|
config_only,
|
||||||
@@ -778,7 +798,10 @@ def export(
|
|||||||
preview_if_missing,
|
preview_if_missing,
|
||||||
profile,
|
profile,
|
||||||
profile_sort,
|
profile_sort,
|
||||||
debug,
|
theme,
|
||||||
|
debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||||
|
watch,
|
||||||
|
breakpoint,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -792,9 +815,10 @@ def export(
|
|||||||
to modify this behavior.
|
to modify this behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if is_debug():
|
# capture locals for use with ConfigOptions before changing any of them
|
||||||
set_debug(True)
|
locals_ = locals()
|
||||||
osxphotos._set_debug(True)
|
|
||||||
|
set_crash_data("locals", locals_)
|
||||||
|
|
||||||
if profile:
|
if profile:
|
||||||
click.echo("Profiling...")
|
click.echo("Profiling...")
|
||||||
@@ -819,22 +843,32 @@ def export(
|
|||||||
# do so below after load_config and save_config are handled.
|
# do so below after load_config and save_config are handled.
|
||||||
cfg = ConfigOptions(
|
cfg = ConfigOptions(
|
||||||
"export",
|
"export",
|
||||||
locals(),
|
locals_,
|
||||||
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
|
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
|
||||||
)
|
)
|
||||||
|
|
||||||
verbose_ = verbose_print(verbose, timestamp)
|
color_theme = get_theme(theme)
|
||||||
|
verbose_ = verbose_print(
|
||||||
|
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||||
|
)
|
||||||
|
# set console for rich_echo to be same as for verbose_
|
||||||
|
set_rich_console(get_verbose_console())
|
||||||
|
set_rich_theme(color_theme)
|
||||||
|
set_rich_timestamp(timestamp)
|
||||||
|
|
||||||
if load_config:
|
if load_config:
|
||||||
try:
|
try:
|
||||||
cfg.load_from_file(load_config)
|
cfg.load_from_file(load_config)
|
||||||
except ConfigOptionsLoadError as e:
|
except ConfigOptionsLoadError as e:
|
||||||
click.echo(
|
# click.echo(
|
||||||
click.style(
|
# click.style(
|
||||||
f"Error parsing {load_config} config file: {e.message}",
|
# f"Error parsing {load_config} config file: {e.message}",
|
||||||
fg=CLI_COLOR_ERROR,
|
# fg=CLI_COLOR_ERROR,
|
||||||
),
|
# ),
|
||||||
err=True,
|
# err=True,
|
||||||
|
# )
|
||||||
|
rich_click_echo(
|
||||||
|
f"[error]Error parsing {load_config} config file: {e.message}", err=True
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -904,6 +938,7 @@ def export(
|
|||||||
no_likes = cfg.no_likes
|
no_likes = cfg.no_likes
|
||||||
no_location = cfg.no_location
|
no_location = cfg.no_location
|
||||||
no_place = cfg.no_place
|
no_place = cfg.no_place
|
||||||
|
no_progress = cfg.no_progress
|
||||||
no_title = cfg.no_title
|
no_title = cfg.no_title
|
||||||
not_burst = cfg.not_burst
|
not_burst = cfg.not_burst
|
||||||
not_favorite = cfg.not_favorite
|
not_favorite = cfg.not_favorite
|
||||||
@@ -956,9 +991,11 @@ def export(
|
|||||||
skip_uuid_from_file = cfg.skip_uuid_from_file
|
skip_uuid_from_file = cfg.skip_uuid_from_file
|
||||||
slow_mo = cfg.slow_mo
|
slow_mo = cfg.slow_mo
|
||||||
strip = cfg.strip
|
strip = cfg.strip
|
||||||
|
theme = cfg.theme
|
||||||
time_lapse = cfg.time_lapse
|
time_lapse = cfg.time_lapse
|
||||||
timestamp = cfg.timestamp
|
timestamp = cfg.timestamp
|
||||||
title = cfg.title
|
title = cfg.title
|
||||||
|
tmpdir = cfg.tmpdir
|
||||||
to_date = cfg.to_date
|
to_date = cfg.to_date
|
||||||
to_time = cfg.to_time
|
to_time = cfg.to_time
|
||||||
touch_file = cfg.touch_file
|
touch_file = cfg.touch_file
|
||||||
@@ -972,8 +1009,17 @@ def export(
|
|||||||
xattr_template = cfg.xattr_template
|
xattr_template = cfg.xattr_template
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
verbose_ = verbose_print(verbose, timestamp)
|
color_theme = get_theme(theme)
|
||||||
verbose_(f"Loaded options from file {load_config}")
|
verbose_ = verbose_print(
|
||||||
|
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||||
|
)
|
||||||
|
# set console for rich_echo to be same as for verbose_
|
||||||
|
set_rich_console(get_verbose_console())
|
||||||
|
set_rich_timestamp(timestamp)
|
||||||
|
|
||||||
|
verbose_(f"Loaded options from file [filepath]{load_config}")
|
||||||
|
|
||||||
|
set_crash_data("cfg", cfg.asdict())
|
||||||
|
|
||||||
verbose_(f"osxphotos version {__version__}")
|
verbose_(f"osxphotos version {__version__}")
|
||||||
|
|
||||||
@@ -1018,28 +1064,22 @@ def export(
|
|||||||
try:
|
try:
|
||||||
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
||||||
except ConfigOptionsInvalidError as e:
|
except ConfigOptionsInvalidError as e:
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
f"[error]Incompatible export options: {e.message}",
|
||||||
f"Incompatible export options: {e.message}", fg=CLI_COLOR_ERROR
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if config_only and not save_config:
|
if config_only and not save_config:
|
||||||
click.secho(
|
rich_click_echo(
|
||||||
"--config-only must be used with --save-config",
|
"[error]--config-only must be used with --save-config",
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]):
|
if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]):
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
"[error]Cannot use --sidecar json with --sidecar exiftool due to name collisions",
|
||||||
"Cannot use --sidecar json with --sidecar exiftool due to name collisions",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1047,21 +1087,18 @@ def export(
|
|||||||
if xattr_template:
|
if xattr_template:
|
||||||
for attr, _ in xattr_template:
|
for attr, _ in xattr_template:
|
||||||
if attr not in EXTENDED_ATTRIBUTE_NAMES:
|
if attr not in EXTENDED_ATTRIBUTE_NAMES:
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
f"[error]Invalid attribute '{attr}' for --xattr-template; "
|
||||||
f"Invalid attribute '{attr}' for --xattr-template; "
|
f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}",
|
||||||
f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if save_config:
|
if save_config:
|
||||||
verbose_(f"Saving options to config file '{save_config}'")
|
verbose_(f"Saving options to config file '[filepath]{save_config}'")
|
||||||
cfg.write_to_file(save_config)
|
cfg.write_to_file(save_config)
|
||||||
if config_only:
|
if config_only:
|
||||||
click.echo(f"Saved config file to '{save_config}'")
|
rich_echo(f"Saved config file to '[filepath]{save_config}'")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# set defaults for options that need them
|
# set defaults for options that need them
|
||||||
@@ -1076,18 +1113,14 @@ def export(
|
|||||||
retry = 0 if not retry else retry
|
retry = 0 if not retry else retry
|
||||||
|
|
||||||
if not os.path.isdir(dest):
|
if not os.path.isdir(dest):
|
||||||
click.echo(
|
rich_click_echo(f"[error]DEST {dest} must be valid path", err=True)
|
||||||
click.style(f"DEST {dest} must be valid path", fg=CLI_COLOR_ERROR), err=True
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
dest = str(pathlib.Path(dest).resolve())
|
dest = str(pathlib.Path(dest).resolve())
|
||||||
|
|
||||||
if report and os.path.isdir(report):
|
if report and os.path.isdir(report):
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
f"[error]report is a directory, must be file name",
|
||||||
f"report is a directory, must be file name", fg=CLI_COLOR_ERROR
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1120,18 +1153,15 @@ def export(
|
|||||||
try:
|
try:
|
||||||
exiftool_path = get_exiftool_path()
|
exiftool_path = get_exiftool_path()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
"[error]Could not find exiftool. Please download and install"
|
||||||
"Could not find exiftool. Please download and install"
|
" from https://exiftool.org/",
|
||||||
" from https://exiftool.org/",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
ctx.exit(2)
|
ctx.exit(1)
|
||||||
|
|
||||||
if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]):
|
if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]):
|
||||||
verbose_(f"exiftool path: {exiftool_path}")
|
verbose_(f"exiftool path: [filepath]{exiftool_path}")
|
||||||
|
|
||||||
# default searches for everything
|
# default searches for everything
|
||||||
photos = True
|
photos = True
|
||||||
@@ -1151,26 +1181,24 @@ def export(
|
|||||||
cli_db = cli_obj.db if cli_obj is not None else None
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
db = get_photos_db(*photos_library, db, cli_db)
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
if not db:
|
if not db:
|
||||||
click.echo(get_help_msg(export), err=True)
|
rich_click_echo(get_help_msg(export), err=True)
|
||||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
rich_click_echo(
|
||||||
|
"\n\nLocated the following Photos library databases: ", err=True
|
||||||
|
)
|
||||||
_list_libraries()
|
_list_libraries()
|
||||||
return
|
return
|
||||||
|
|
||||||
# sanity check exportdb
|
# sanity check exportdb
|
||||||
if exportdb and exportdb != OSXPHOTOS_EXPORT_DB:
|
if exportdb and exportdb != OSXPHOTOS_EXPORT_DB:
|
||||||
if pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists():
|
if pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists():
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
f"[warning]Warning: export database is '{exportdb}' but found '{OSXPHOTOS_EXPORT_DB}' in {dest}; using '{exportdb}'",
|
||||||
f"Warning: export database is '{exportdb}' but found '{OSXPHOTOS_EXPORT_DB}' in {dest}; using '{exportdb}'",
|
err=True,
|
||||||
fg=CLI_COLOR_WARNING,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if pathlib.Path(exportdb).resolve().parent != pathlib.Path(dest):
|
if pathlib.Path(exportdb).resolve().parent != pathlib.Path(dest):
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
f"[warning]Warning: export database '{pathlib.Path(exportdb).resolve()}' is in a different directory than export destination '{dest}'",
|
||||||
f"Warning: export database '{pathlib.Path(exportdb).resolve()}' is in a different directory than export destination '{dest}'",
|
err=True,
|
||||||
fg=CLI_COLOR_WARNING,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# open export database
|
# open export database
|
||||||
@@ -1179,21 +1207,18 @@ def export(
|
|||||||
# check that export isn't in the parent or child of a previously exported library
|
# check that export isn't in the parent or child of a previously exported library
|
||||||
other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB)
|
other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB)
|
||||||
if other_db_files:
|
if other_db_files:
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
click.style(
|
"[warning]WARNING: found other export database files in this destination directory branch. "
|
||||||
"WARNING: found other export database files in this destination directory branch. "
|
+ "This likely means you are attempting to export files into a directory "
|
||||||
+ "This likely means you are attempting to export files into a directory "
|
+ "that is either the parent or a child directory of a previous export. "
|
||||||
+ "that is either the parent or a child directory of a previous export. "
|
+ "Proceeding may cause your exported files to be overwritten.",
|
||||||
+ "Proceeding may cause your exported files to be overwritten.",
|
|
||||||
fg=CLI_COLOR_WARNING,
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
click.echo(
|
rich_click_echo(
|
||||||
f"You are exporting to {dest}, found {OSXPHOTOS_EXPORT_DB} files in:"
|
f"You are exporting to {dest}, found {OSXPHOTOS_EXPORT_DB} files in:"
|
||||||
)
|
)
|
||||||
for other_db in other_db_files:
|
for other_db in other_db_files:
|
||||||
click.echo(f"{other_db}")
|
rich_click_echo(f"{other_db}")
|
||||||
click.confirm("Do you want to continue?", abort=True)
|
click.confirm("Do you want to continue?", abort=True)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -1209,19 +1234,21 @@ def export(
|
|||||||
|
|
||||||
if verbose_:
|
if verbose_:
|
||||||
if export_db.was_created:
|
if export_db.was_created:
|
||||||
verbose_(f"Created export database {export_db_path}")
|
verbose_(f"Created export database [filepath]{export_db_path}")
|
||||||
else:
|
else:
|
||||||
verbose_(f"Using export database {export_db_path}")
|
verbose_(f"Using export database [filepath]{export_db_path}")
|
||||||
upgraded = export_db.was_upgraded
|
upgraded = export_db.was_upgraded
|
||||||
if upgraded:
|
if upgraded:
|
||||||
verbose_(
|
verbose_(
|
||||||
f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}"
|
f"Upgraded export database [filepath]{export_db_path}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]"
|
||||||
)
|
)
|
||||||
|
|
||||||
# save config to export_db
|
# save config to export_db
|
||||||
export_db.set_config(cfg.write_to_str())
|
export_db.set_config(cfg.write_to_str())
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path)
|
photosdb = osxphotos.PhotosDB(
|
||||||
|
dbfile=db, verbose=verbose_, exiftool=exiftool_path, rich=True
|
||||||
|
)
|
||||||
|
|
||||||
# enable beta features if requested
|
# enable beta features if requested
|
||||||
photosdb._beta = beta
|
photosdb._beta = beta
|
||||||
@@ -1335,7 +1362,9 @@ def export(
|
|||||||
num_photos = len(photos)
|
num_photos = len(photos)
|
||||||
# TODO: photos or photo appears several times, pull into a separate function
|
# TODO: photos or photo appears several times, pull into a separate function
|
||||||
photo_str = "photos" if num_photos > 1 else "photo"
|
photo_str = "photos" if num_photos > 1 else "photo"
|
||||||
click.echo(f"Exporting {num_photos} {photo_str} to {dest}...")
|
rich_echo(
|
||||||
|
f"Exporting [num]{num_photos}[/num] {photo_str} to [filepath]{dest}[/]..."
|
||||||
|
)
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
# though the command line option is current_name, internally all processing
|
# though the command line option is current_name, internally all processing
|
||||||
# logic uses original_name which is the boolean inverse of current_name
|
# logic uses original_name which is the boolean inverse of current_name
|
||||||
@@ -1360,10 +1389,11 @@ def export(
|
|||||||
)
|
)
|
||||||
|
|
||||||
photo_num = 0
|
photo_num = 0
|
||||||
# send progress bar output to /dev/null if verbose to hide the progress bar
|
with rich_progress(console=get_verbose_console(), mock=no_progress) as progress:
|
||||||
fp = open(os.devnull, "w") if verbose else None
|
task = progress.add_task(
|
||||||
with click.progressbar(photos, show_pos=True, file=fp) as bar:
|
f"Exporting [num]{num_photos}[/] photos", total=num_photos
|
||||||
for p in bar:
|
)
|
||||||
|
for p in photos:
|
||||||
photo_num += 1
|
photo_num += 1
|
||||||
export_results = export_photo(
|
export_results = export_photo(
|
||||||
photo=p,
|
photo=p,
|
||||||
@@ -1414,20 +1444,19 @@ def export(
|
|||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
verbose_=verbose_,
|
verbose_=verbose_,
|
||||||
|
tmpdir=tmpdir,
|
||||||
)
|
)
|
||||||
|
|
||||||
if post_function:
|
if post_function:
|
||||||
for function in post_function:
|
for function in post_function:
|
||||||
# post function is tuple of (function, filename.py::function_name)
|
# post function is tuple of (function, filename.py::function_name)
|
||||||
verbose_(f"Calling post-function {function[1]}")
|
verbose_(f"Calling post-function [bold]{function[1]}")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
try:
|
try:
|
||||||
function[0](p, export_results, verbose_)
|
function[0](p, export_results, verbose_)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.secho(
|
rich_echo_error(
|
||||||
f"Error running post-function {function[1]}: {e}",
|
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
run_post_command(
|
run_post_command(
|
||||||
@@ -1525,32 +1554,31 @@ def export(
|
|||||||
results.xattr_written.extend(xattr_written)
|
results.xattr_written.extend(xattr_written)
|
||||||
results.xattr_skipped.extend(xattr_skipped)
|
results.xattr_skipped.extend(xattr_skipped)
|
||||||
|
|
||||||
if fp is not None:
|
progress.advance(task)
|
||||||
fp.close()
|
|
||||||
|
|
||||||
photo_str_total = "photos" if len(photos) != 1 else "photo"
|
photo_str_total = "photos" if len(photos) != 1 else "photo"
|
||||||
if update or force_update:
|
if update or force_update:
|
||||||
summary = (
|
summary = (
|
||||||
f"Processed: {len(photos)} {photo_str_total}, "
|
f"Processed: [num]{len(photos)}[/] {photo_str_total}, "
|
||||||
f"exported: {len(results.new)}, "
|
f"exported: [num]{len(results.new)}[/], "
|
||||||
f"updated: {len(results.updated)}, "
|
f"updated: [num]{len(results.updated)}[/], "
|
||||||
f"skipped: {len(results.skipped)}, "
|
f"skipped: [num]{len(results.skipped)}[/], "
|
||||||
f"updated EXIF data: {len(results.exif_updated)}, "
|
f"updated EXIF data: [num]{len(results.exif_updated)}[/], "
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
summary = (
|
summary = (
|
||||||
f"Processed: {len(photos)} {photo_str_total}, "
|
f"Processed: [num]{len(photos)}[/] {photo_str_total}, "
|
||||||
f"exported: {len(results.exported)}, "
|
f"exported: [num]{len(results.exported)}[/], "
|
||||||
)
|
)
|
||||||
summary += f"missing: {len(results.missing)}, "
|
summary += f"missing: [num]{len(results.missing)}[/], "
|
||||||
summary += f"error: {len(results.error)}"
|
summary += f"error: [num]{len(results.error)}[/]"
|
||||||
if touch_file:
|
if touch_file:
|
||||||
summary += f", touched date: {len(results.touched)}"
|
summary += f", touched date: [num]{len(results.touched)}[/]"
|
||||||
click.echo(summary)
|
rich_echo(summary)
|
||||||
stop_time = time.perf_counter()
|
stop_time = time.perf_counter()
|
||||||
click.echo(f"Elapsed time: {format_sec_to_hhmmss(stop_time-start_time)}")
|
rich_echo(f"Elapsed time: [time]{format_sec_to_hhmmss(stop_time-start_time)}")
|
||||||
else:
|
else:
|
||||||
click.echo("Did not find any photos to export")
|
rich_echo("Did not find any photos to export")
|
||||||
|
|
||||||
# cleanup files and do report if needed
|
# cleanup files and do report if needed
|
||||||
if cleanup:
|
if cleanup:
|
||||||
@@ -1576,25 +1604,25 @@ def export(
|
|||||||
+ [r[0] for r in results.error]
|
+ [r[0] for r in results.error]
|
||||||
+ db_files
|
+ db_files
|
||||||
)
|
)
|
||||||
click.echo(f"Cleaning up {dest}")
|
rich_echo(f"Cleaning up [filepath]{dest}")
|
||||||
cleaned_files, cleaned_dirs = cleanup_files(
|
cleaned_files, cleaned_dirs = cleanup_files(
|
||||||
dest, all_files, fileutil, verbose_=verbose_
|
dest, all_files, fileutil, verbose_=verbose_
|
||||||
)
|
)
|
||||||
file_str = "files" if len(cleaned_files) != 1 else "file"
|
file_str = "files" if len(cleaned_files) != 1 else "file"
|
||||||
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
|
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
|
||||||
click.echo(
|
rich_echo(
|
||||||
f"Deleted: {len(cleaned_files)} {file_str}, {len(cleaned_dirs)} {dir_str}"
|
f"Deleted: [num]{len(cleaned_files)}[/num] {file_str}, [num]{len(cleaned_dirs)}[/num] {dir_str}"
|
||||||
)
|
)
|
||||||
results.deleted_files = cleaned_files
|
results.deleted_files = cleaned_files
|
||||||
results.deleted_directories = cleaned_dirs
|
results.deleted_directories = cleaned_dirs
|
||||||
|
|
||||||
if report:
|
if report:
|
||||||
verbose_(f"Writing export report to {report}")
|
verbose_(f"Writing export report to [filepath]{report}")
|
||||||
write_export_report(report, results)
|
write_export_report(report, results)
|
||||||
|
|
||||||
# close export_db and write changes if needed
|
# close export_db and write changes if needed
|
||||||
if ramdb and not dry_run:
|
if ramdb and not dry_run:
|
||||||
verbose_(f"Writing export database changes back to {export_db.path}")
|
verbose_(f"Writing export database changes back to [filepath]{export_db.path}")
|
||||||
export_db.write_to_disk()
|
export_db.write_to_disk()
|
||||||
export_db.close()
|
export_db.close()
|
||||||
|
|
||||||
@@ -1660,6 +1688,7 @@ def export_photo(
|
|||||||
preview_if_missing=False,
|
preview_if_missing=False,
|
||||||
photo_num=1,
|
photo_num=1,
|
||||||
num_photos=1,
|
num_photos=1,
|
||||||
|
tmpdir=None,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""Helper function for export that does the actual export
|
||||||
|
|
||||||
@@ -1707,6 +1736,7 @@ def export_photo(
|
|||||||
update: bool, only export updated photos
|
update: bool, only export updated photos
|
||||||
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
|
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
|
||||||
verbose_: callable for verbose output
|
verbose_: callable for verbose output
|
||||||
|
tmpdir: optional str; temporary directory to use for export
|
||||||
Returns:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
list of path(s) of exported photo or None if photo was missing
|
||||||
|
|
||||||
@@ -1729,7 +1759,7 @@ def export_photo(
|
|||||||
export_original = True
|
export_original = True
|
||||||
export_edited = False
|
export_edited = False
|
||||||
verbose_(
|
verbose_(
|
||||||
f"Edited file for {photo.original_filename} is missing, exporting original"
|
f"Edited file for [filename]{photo.original_filename}[/] is missing, exporting original"
|
||||||
)
|
)
|
||||||
|
|
||||||
# check for missing photos before downloading
|
# check for missing photos before downloading
|
||||||
@@ -1825,7 +1855,7 @@ def export_photo(
|
|||||||
original_filename = str(original_filename)
|
original_filename = str(original_filename)
|
||||||
|
|
||||||
verbose_(
|
verbose_(
|
||||||
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename} ({photo_num}/{num_photos})"
|
f"Exporting [filename]{photo.original_filename}[/] ([filename]{photo.filename}[/]) as [filepath]{original_filename}[/] ([count]{photo_num}/{num_photos}[/])"
|
||||||
)
|
)
|
||||||
|
|
||||||
results += export_photo_to_directory(
|
results += export_photo_to_directory(
|
||||||
@@ -1871,6 +1901,7 @@ def export_photo(
|
|||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose_=verbose_,
|
verbose_=verbose_,
|
||||||
|
tmpdir=tmpdir,
|
||||||
)
|
)
|
||||||
|
|
||||||
if export_edited and photo.hasadjustments:
|
if export_edited and photo.hasadjustments:
|
||||||
@@ -1938,7 +1969,7 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
verbose_(
|
verbose_(
|
||||||
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
|
f"Exporting edited version of [filename]{photo.original_filename}[/filename] ([filename]{photo.filename}[/filename]) as [filepath]{edited_filename}[/filepath]"
|
||||||
)
|
)
|
||||||
|
|
||||||
results += export_photo_to_directory(
|
results += export_photo_to_directory(
|
||||||
@@ -1984,6 +2015,7 @@ def export_photo(
|
|||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose_=verbose_,
|
verbose_=verbose_,
|
||||||
|
tmpdir=tmpdir,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -2068,6 +2100,7 @@ def export_photo_to_directory(
|
|||||||
use_photos_export,
|
use_photos_export,
|
||||||
use_photokit,
|
use_photokit,
|
||||||
verbose_,
|
verbose_,
|
||||||
|
tmpdir,
|
||||||
):
|
):
|
||||||
"""Export photo to directory dest_path"""
|
"""Export photo to directory dest_path"""
|
||||||
|
|
||||||
@@ -2087,7 +2120,7 @@ def export_photo_to_directory(
|
|||||||
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
|
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
|
||||||
|
|
||||||
if not export_original and not edited:
|
if not export_original and not edited:
|
||||||
verbose_(f"Skipping original version of {photo.original_filename}")
|
verbose_(f"Skipping original version of [filename]{photo.original_filename}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
tries = 0
|
tries = 0
|
||||||
@@ -2130,69 +2163,62 @@ def export_photo_to_directory(
|
|||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
|
tmpdir=tmpdir,
|
||||||
|
rich=True,
|
||||||
)
|
)
|
||||||
exporter = PhotoExporter(photo)
|
exporter = PhotoExporter(photo)
|
||||||
export_results = exporter.export(
|
export_results = exporter.export(
|
||||||
dest=dest_path, filename=filename, options=export_options
|
dest=dest_path, filename=filename, options=export_options
|
||||||
)
|
)
|
||||||
for warning_ in export_results.exiftool_warning:
|
for warning_ in export_results.exiftool_warning:
|
||||||
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
|
verbose_(
|
||||||
|
f"[warning]exiftool warning for file {warning_[0]}: {warning_[1]}"
|
||||||
|
)
|
||||||
for error_ in export_results.exiftool_error:
|
for error_ in export_results.exiftool_error:
|
||||||
click.echo(
|
rich_echo_error(
|
||||||
click.style(
|
f"[error]exiftool error for file {error_[0]}: {error_[1]}"
|
||||||
f"exiftool error for file {error_[0]}: {error_[1]}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
for error_ in export_results.error:
|
for error_ in export_results.error:
|
||||||
click.echo(
|
rich_echo_error(
|
||||||
click.style(
|
f"[error]Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}"
|
||||||
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
error += 1
|
error += 1
|
||||||
if not error or tries > retry:
|
if not error or tries > retry:
|
||||||
results += export_results
|
results += export_results
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
click.echo(
|
rich_echo(
|
||||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
f"Retrying export for photo ([uuid]{photo.uuid}[/uuid]: [filename]{photo.original_filename}[/filename])"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if is_debug():
|
if is_debug():
|
||||||
# if debug mode, don't swallow the exceptions
|
# if debug mode, don't swallow the exceptions
|
||||||
raise e
|
raise e
|
||||||
click.echo(
|
rich_echo(
|
||||||
click.style(
|
f"[error]Error exporting photo ([uuid]{photo.uuid}[/uuid]: [filename]{photo.original_filename}[/filename]) as [filepath]{filename}[/filepath]: {e}",
|
||||||
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {filename}: {e}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
if tries > retry:
|
if tries > retry:
|
||||||
results.error.append((str(pathlib.Path(dest) / filename), e))
|
results.error.append((str(pathlib.Path(dest) / filename), e))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
click.echo(
|
rich_echo(
|
||||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
f"Retrying export for photo ([uuid]{photo.uuid}[/uuid]: [filename]{photo.original_filename}[/filename])"
|
||||||
)
|
)
|
||||||
|
|
||||||
if verbose_:
|
if verbose_:
|
||||||
if update or force_update:
|
if update or force_update:
|
||||||
for new in results.new:
|
for new in results.new:
|
||||||
verbose_(f"Exported new file {new}")
|
verbose_(f"Exported new file [filepath]{new}")
|
||||||
for updated in results.updated:
|
for updated in results.updated:
|
||||||
verbose_(f"Exported updated file {updated}")
|
verbose_(f"Exported updated file [filepath]{updated}")
|
||||||
for skipped in results.skipped:
|
for skipped in results.skipped:
|
||||||
verbose_(f"Skipped up to date file {skipped}")
|
verbose_(f"Skipped up to date file [filepath]{skipped}")
|
||||||
else:
|
else:
|
||||||
for exported in results.exported:
|
for exported in results.exported:
|
||||||
verbose_(f"Exported {exported}")
|
verbose_(f"Exported [filepath]{exported}")
|
||||||
for touched in results.touched:
|
for touched in results.touched:
|
||||||
verbose_(f"Touched date on file {touched}")
|
verbose_(f"Touched date on file [filepath]{touched}")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -2502,10 +2528,7 @@ def write_export_report(report_file, results):
|
|||||||
for data in [result for result in all_results.values()]:
|
for data in [result for result in all_results.values()]:
|
||||||
writer.writerow(data)
|
writer.writerow(data)
|
||||||
except IOError:
|
except IOError:
|
||||||
click.echo(
|
rich_echo_error("[error]Could not open output file for writing"),
|
||||||
click.style("Could not open output file for writing", fg=CLI_COLOR_ERROR),
|
|
||||||
err=True,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -2528,7 +2551,7 @@ def cleanup_files(dest_path, files_to_keep, fileutil, verbose_):
|
|||||||
deleted_files = []
|
deleted_files = []
|
||||||
for p in pathlib.Path(dest_path).rglob("*"):
|
for p in pathlib.Path(dest_path).rglob("*"):
|
||||||
if p.is_file() and normalize_fs_path(str(p).lower()) not in keepers:
|
if p.is_file() and normalize_fs_path(str(p).lower()) not in keepers:
|
||||||
verbose_(f"Deleting {p}")
|
verbose_(f"Deleting [filepath]{p}")
|
||||||
fileutil.unlink(p)
|
fileutil.unlink(p)
|
||||||
deleted_files.append(str(p))
|
deleted_files.append(str(p))
|
||||||
|
|
||||||
@@ -2586,6 +2609,7 @@ def write_finder_tags(
|
|||||||
use_persons_as_keywords=person_keyword,
|
use_persons_as_keywords=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
merge_exif_keywords=exiftool_merge_keywords,
|
merge_exif_keywords=exiftool_merge_keywords,
|
||||||
|
rich=True,
|
||||||
)
|
)
|
||||||
exif = PhotoExporter(photo)._exiftool_dict(options=export_options)
|
exif = PhotoExporter(photo)._exiftool_dict(options=export_options)
|
||||||
try:
|
try:
|
||||||
@@ -2611,12 +2635,8 @@ def write_finder_tags(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
rich_echo(
|
||||||
click.style(
|
f"[warning]Warning: unknown field for template: {template_str} unknown field = {unmatched}"
|
||||||
f"Warning: unknown field for template: {template_str} unknown field = {unmatched}",
|
|
||||||
fg=CLI_COLOR_WARNING,
|
|
||||||
),
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
rendered_tags.extend(rendered)
|
rendered_tags.extend(rendered)
|
||||||
|
|
||||||
@@ -2675,12 +2695,8 @@ def write_extended_attributes(
|
|||||||
f"Invalid template for --xattr-template '{template_str}': {e}",
|
f"Invalid template for --xattr-template '{template_str}': {e}",
|
||||||
)
|
)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
rich_echo(
|
||||||
click.style(
|
f"[warning]Warning: unmatched template substitution for template: {template_str} unknown field={unmatched}"
|
||||||
f"Warning: unmatched template substitution for template: {template_str} unknown field={unmatched}",
|
|
||||||
fg=CLI_COLOR_WARNING,
|
|
||||||
),
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# filter out any template values that didn't match by looking for sentinel
|
# filter out any template values that didn't match by looking for sentinel
|
||||||
@@ -2758,10 +2774,6 @@ def run_post_command(
|
|||||||
finally:
|
finally:
|
||||||
run_error = run_error or run_results.returncode
|
run_error = run_error or run_results.returncode
|
||||||
if run_error:
|
if run_error:
|
||||||
click.echo(
|
rich_echo_error(
|
||||||
click.style(
|
f'[error]Error running command "{command}": {run_error}'
|
||||||
f'Error running command "{command}": {run_error}',
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
),
|
|
||||||
err=True,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ from osxphotos.export_db_utils import (
|
|||||||
export_db_vacuum,
|
export_db_vacuum,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .common import OSXPHOTOS_HIDDEN, verbose_print
|
from .common import OSXPHOTOS_HIDDEN
|
||||||
|
from .verbose import verbose_print
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="exportdb", hidden=OSXPHOTOS_HIDDEN)
|
@click.command(name="exportdb", hidden=OSXPHOTOS_HIDDEN)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Help text helper class for osxphotos CLI """
|
"""Help text helper class for osxphotos CLI """
|
||||||
|
|
||||||
import io
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
import typing as t
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import osxmetadata
|
import osxmetadata
|
||||||
@@ -21,6 +22,13 @@ from osxphotos.phototemplate import (
|
|||||||
get_template_help,
|
get_template_help,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_echo_via_pager
|
||||||
|
from .color_themes import get_theme
|
||||||
|
from .common import OSXPHOTOS_HIDDEN
|
||||||
|
|
||||||
|
HELP_WIDTH = 110
|
||||||
|
HIGHLIGHT_COLOR = "yellow"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ExportCommand",
|
"ExportCommand",
|
||||||
"template_help",
|
"template_help",
|
||||||
@@ -40,19 +48,161 @@ def get_help_msg(command):
|
|||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--width",
|
||||||
|
default=HELP_WIDTH,
|
||||||
|
help="Width of help text",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
)
|
||||||
@click.argument("topic", default=None, required=False, nargs=1)
|
@click.argument("topic", default=None, required=False, nargs=1)
|
||||||
|
@click.argument("subtopic", default=None, required=False, nargs=1)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def help(ctx, topic, **kw):
|
def help(ctx, topic, subtopic, width, **kw):
|
||||||
"""Print help; for help on commands: help <command>."""
|
"""Print help; for help on commands: help <command>."""
|
||||||
if topic is None:
|
if topic is None:
|
||||||
click.echo(ctx.parent.get_help())
|
click.echo(ctx.parent.get_help())
|
||||||
return
|
return
|
||||||
elif topic in ctx.obj.group.commands:
|
|
||||||
|
global HELP_WIDTH
|
||||||
|
HELP_WIDTH = width
|
||||||
|
|
||||||
|
wrap_text_original = click.formatting.wrap_text
|
||||||
|
|
||||||
|
def wrap_text(
|
||||||
|
text: str,
|
||||||
|
width: int = HELP_WIDTH,
|
||||||
|
initial_indent: str = "",
|
||||||
|
subsequent_indent: str = "",
|
||||||
|
preserve_paragraphs: bool = False,
|
||||||
|
) -> str:
|
||||||
|
return wrap_text_original(
|
||||||
|
text,
|
||||||
|
width=width,
|
||||||
|
initial_indent=initial_indent,
|
||||||
|
subsequent_indent=subsequent_indent,
|
||||||
|
preserve_paragraphs=preserve_paragraphs,
|
||||||
|
)
|
||||||
|
|
||||||
|
click.formatting.wrap_text = wrap_text
|
||||||
|
click.wrap_text = wrap_text
|
||||||
|
|
||||||
|
if subtopic:
|
||||||
|
cmd = ctx.obj.group.commands[topic]
|
||||||
|
rich_echo_via_pager(
|
||||||
|
get_subtopic_help(cmd, ctx, subtopic),
|
||||||
|
theme=get_theme(),
|
||||||
|
width=HELP_WIDTH,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if topic in ctx.obj.group.commands:
|
||||||
ctx.info_name = topic
|
ctx.info_name = topic
|
||||||
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
|
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
|
||||||
|
return
|
||||||
|
|
||||||
|
# didn't find any valid help topics
|
||||||
|
click.echo(f"Invalid command: {topic}", err=True)
|
||||||
|
click.echo(ctx.parent.get_help())
|
||||||
|
|
||||||
|
|
||||||
|
def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
|
||||||
|
"""Get help for a command including only options that match a subtopic"""
|
||||||
|
|
||||||
|
# set ctx.info_name or click prints the wrong usage str (usage for help instead of cmd)
|
||||||
|
ctx.info_name = cmd.name
|
||||||
|
usage_str = cmd.get_help(ctx)
|
||||||
|
usage_str = usage_str.partition("\n")[0]
|
||||||
|
|
||||||
|
info = cmd.to_info_dict(ctx)
|
||||||
|
help_str = info.get("help", "")
|
||||||
|
|
||||||
|
options = get_matching_options(cmd, ctx, subtopic)
|
||||||
|
|
||||||
|
# format help text and options
|
||||||
|
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||||
|
formatter.write(usage_str)
|
||||||
|
formatter.write_paragraph()
|
||||||
|
format_help_text(help_str, formatter)
|
||||||
|
formatter.write_paragraph()
|
||||||
|
if options:
|
||||||
|
option_str = format_options_help(options, ctx, highlight=subtopic)
|
||||||
|
formatter.write(f"Options that match '[highlight]{subtopic}[/highlight]':\n")
|
||||||
|
formatter.write_paragraph()
|
||||||
|
formatter.write(option_str)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Invalid command: {topic}", err=True)
|
formatter.write(f"No options match '[highlight]{subtopic}[/highlight]'")
|
||||||
click.echo(ctx.parent.get_help())
|
return formatter.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def get_matching_options(
|
||||||
|
command: click.Command, ctx: click.Context, topic: str
|
||||||
|
) -> t.List:
|
||||||
|
"""Get matching options for a command that contain a topic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: click.Command
|
||||||
|
ctx: click.Context
|
||||||
|
topic: str, topic to match
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of matching click.Option objects
|
||||||
|
|
||||||
|
"""
|
||||||
|
options = []
|
||||||
|
topic = topic.lower()
|
||||||
|
for option in command.params:
|
||||||
|
help_record = option.get_help_record(ctx)
|
||||||
|
if help_record and (topic in help_record[0] or topic in help_record[1]):
|
||||||
|
options.append(option)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def format_options_help(
|
||||||
|
options: t.List[click.Option], ctx: click.Context, highlight: t.Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""Format options help for display
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: list of click.Option objects
|
||||||
|
ctx: click.Context
|
||||||
|
highlight: str, if set, add rich highlighting to options that match highlight str
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str with formatted help
|
||||||
|
|
||||||
|
"""
|
||||||
|
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||||
|
opt_help = [opt.get_help_record(ctx) for opt in options]
|
||||||
|
if highlight:
|
||||||
|
# convert list of tuples to list of lists
|
||||||
|
opt_help = [list(opt) for opt in opt_help]
|
||||||
|
for record in opt_help:
|
||||||
|
record[0] = re.sub(
|
||||||
|
f"({highlight})",
|
||||||
|
"[highlight]\\1[/highlight]",
|
||||||
|
record[0],
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
record[1] = re.sub(
|
||||||
|
f"({highlight})",
|
||||||
|
"[highlight]\\1[/highlight]",
|
||||||
|
record[1],
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# convert back to list of tuples as that's what write_dl expects
|
||||||
|
opt_help = [tuple(opt) for opt in opt_help]
|
||||||
|
formatter.write_dl(opt_help)
|
||||||
|
return formatter.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def format_help_text(text: str, formatter: click.HelpFormatter):
|
||||||
|
text = inspect.cleandoc(text).partition("\f")[0]
|
||||||
|
formatter.write_paragraph()
|
||||||
|
|
||||||
|
with formatter.indentation():
|
||||||
|
formatter.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
# TODO: The following help text could probably be done as mako template
|
# TODO: The following help text could probably be done as mako template
|
||||||
@@ -61,11 +211,9 @@ class ExportCommand(click.Command):
|
|||||||
|
|
||||||
def get_help(self, ctx):
|
def get_help(self, ctx):
|
||||||
help_text = super().get_help(ctx)
|
help_text = super().get_help(ctx)
|
||||||
formatter = click.HelpFormatter()
|
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||||
# passed to click.HelpFormatter.write_dl for formatting
|
formatter.write("\n")
|
||||||
|
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
|
||||||
formatter.write("\n\n")
|
|
||||||
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
|
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"When exporting photos, osxphotos creates a database in the top-level "
|
"When exporting photos, osxphotos creates a database in the top-level "
|
||||||
@@ -100,9 +248,9 @@ class ExportCommand(click.Command):
|
|||||||
+ "You can always run export without the --update option to re-export the entire library thus "
|
+ "You can always run export without the --update option to re-export the entire library thus "
|
||||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||||
)
|
)
|
||||||
formatter.write("\n\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
|
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
@@ -123,25 +271,33 @@ The following attributes may be used with '--xattr-template':
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
formatter.write_dl(
|
attr_tuples = [
|
||||||
[
|
(
|
||||||
|
rich_text("[bold]Attribute[/bold]", width=formatter.width),
|
||||||
|
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||||
|
),
|
||||||
|
*[
|
||||||
(
|
(
|
||||||
attr,
|
attr,
|
||||||
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
||||||
)
|
)
|
||||||
for attr in EXTENDED_ATTRIBUTE_NAMES
|
for attr in EXTENDED_ATTRIBUTE_NAMES
|
||||||
]
|
],
|
||||||
)
|
]
|
||||||
|
formatter.write_dl(attr_tuples)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||||
)
|
)
|
||||||
formatter.write("\n\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
|
rich_text("## Templating System", width=formatter.width, markdown=True)
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(template_help(width=formatter.width))
|
help_text += formatter.getvalue()
|
||||||
|
help_text += template_help(width=formatter.width)
|
||||||
|
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||||
|
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"With the --directory and --filename options you may specify a template for the "
|
"With the --directory and --filename options you may specify a template for the "
|
||||||
@@ -169,12 +325,15 @@ The following attributes may be used with '--xattr-template':
|
|||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
rich_text(
|
rich_text("## Template Substitutions", width=formatter.width, markdown=True)
|
||||||
"[bold]** Template Substitutions **[/bold]", width=formatter.width
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
templ_tuples = [("Substitution", "Description")]
|
templ_tuples = [
|
||||||
|
(
|
||||||
|
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||||
|
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||||
|
)
|
||||||
|
]
|
||||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
|
|
||||||
@@ -189,7 +348,12 @@ The following attributes may be used with '--xattr-template':
|
|||||||
+ "2019/Vacation, 2019/Family"
|
+ "2019/Vacation, 2019/Family"
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
templ_tuples = [("Substitution", "Description")]
|
templ_tuples = [
|
||||||
|
(
|
||||||
|
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||||
|
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||||
|
)
|
||||||
|
]
|
||||||
templ_tuples.extend(
|
templ_tuples.extend(
|
||||||
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
||||||
)
|
)
|
||||||
@@ -227,10 +391,11 @@ The following attributes may be used with '--xattr-template':
|
|||||||
|
|
||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
|
|
||||||
formatter.write("\n\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
|
rich_text("## Post Command", width=formatter.width, markdown=True)
|
||||||
)
|
)
|
||||||
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"You can run commands on the exported photos for post-processing "
|
"You can run commands on the exported photos for post-processing "
|
||||||
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
|
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
|
||||||
@@ -273,10 +438,11 @@ The following attributes may be used with '--xattr-template':
|
|||||||
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
|
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
|
||||||
+ "print out the exact command string which would be executed."
|
+ "print out the exact command string which would be executed."
|
||||||
)
|
)
|
||||||
formatter.write("\n\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
|
rich_text("## Post Function", width=formatter.width, markdown=True)
|
||||||
)
|
)
|
||||||
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"You can run your own python functions on the exported photos for post-processing "
|
"You can run your own python functions on the exported photos for post-processing "
|
||||||
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
|
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
|
||||||
@@ -294,23 +460,19 @@ The following attributes may be used with '--xattr-template':
|
|||||||
|
|
||||||
def template_help(width=78):
|
def template_help(width=78):
|
||||||
"""Return formatted string for template system"""
|
"""Return formatted string for template system"""
|
||||||
sio = io.StringIO()
|
|
||||||
console = Console(file=sio, force_terminal=True, width=width)
|
|
||||||
template_help_md = strip_md_header_and_links(get_template_help())
|
template_help_md = strip_md_header_and_links(get_template_help())
|
||||||
console.print(Markdown(template_help_md))
|
console = Console(force_terminal=True, width=width)
|
||||||
help_str = sio.getvalue()
|
with console.capture() as capture:
|
||||||
sio.close()
|
console.print(Markdown(template_help_md))
|
||||||
return help_str
|
return capture.get()
|
||||||
|
|
||||||
|
|
||||||
def rich_text(text, width=78):
|
def rich_text(text, width=78, markdown=False):
|
||||||
"""Return rich formatted text"""
|
"""Return rich formatted text"""
|
||||||
sio = io.StringIO()
|
console = Console(force_terminal=True, width=width)
|
||||||
console = Console(file=sio, force_terminal=True, width=width)
|
with console.capture() as capture:
|
||||||
console.print(text)
|
console.print(Markdown(text) if markdown else text, end="")
|
||||||
rich_text = sio.getvalue()
|
return capture.get()
|
||||||
sio.close()
|
|
||||||
return rich_text
|
|
||||||
|
|
||||||
|
|
||||||
def strip_md_header_and_links(md):
|
def strip_md_header_and_links(md):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
from osxphotos.debug import set_debug
|
||||||
from osxphotos.photosalbum import PhotosAlbum
|
from osxphotos.photosalbum import PhotosAlbum
|
||||||
from osxphotos.queryoptions import QueryOptions
|
from osxphotos.queryoptions import QueryOptions
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ from .common import (
|
|||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
get_photos_db,
|
get_photos_db,
|
||||||
load_uuid_from_file,
|
load_uuid_from_file,
|
||||||
set_debug,
|
|
||||||
)
|
)
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
from .print_photo_info import print_photo_info
|
from .print_photo_info import print_photo_info
|
||||||
@@ -149,17 +149,13 @@ def query(
|
|||||||
query_eval,
|
query_eval,
|
||||||
query_function,
|
query_function,
|
||||||
add_to_album,
|
add_to_album,
|
||||||
debug,
|
debug, # handled in cli/__init__.py
|
||||||
):
|
):
|
||||||
"""Query the Photos database using 1 or more search options;
|
"""Query the Photos database using 1 or more search options;
|
||||||
if more than one option is provided, they are treated as "AND"
|
if more than one option is provided, they are treated as "AND"
|
||||||
(e.g. search for photos matching all options).
|
(e.g. search for photos matching all options).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if debug:
|
|
||||||
set_debug(True)
|
|
||||||
osxphotos._set_debug(True)
|
|
||||||
|
|
||||||
# if no query terms, show help and return
|
# if no query terms, show help and return
|
||||||
# sanity check input args
|
# sanity check input args
|
||||||
nonexclusive = [
|
nonexclusive = [
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
|||||||
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportOptions, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
|
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportOptions, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
|
||||||
)
|
)
|
||||||
print("The following variables are defined:")
|
print("The following variables are defined:")
|
||||||
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
|
print(f"- photosdb: PhotosDB() instance for '{photosdb.library_path}'")
|
||||||
print(
|
print(
|
||||||
f"- photos: list of PhotoInfo objects for all photos filtered with any query options passed on command line (len={len(photos)})"
|
f"- photos: list of PhotoInfo objects for all photos filtered with any query options passed on command line (len={len(photos)})"
|
||||||
)
|
)
|
||||||
|
|||||||
68
osxphotos/cli/rich_progress.py
Normal file
68
osxphotos/cli/rich_progress.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""rich Progress bar factory that can return a rich Progress bar or a mock Progress bar"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.progress import GetTimeCallable, Progress, ProgressColumn, TaskID
|
||||||
|
|
||||||
|
# set to 1 if running tests
|
||||||
|
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
|
||||||
|
|
||||||
|
|
||||||
|
class MockProgress:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_task(
|
||||||
|
self,
|
||||||
|
description: str,
|
||||||
|
start: bool = True,
|
||||||
|
total: float = 100.0,
|
||||||
|
completed: int = 0,
|
||||||
|
visible: bool = True,
|
||||||
|
**fields: Any,
|
||||||
|
) -> TaskID:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def advance(self, task_id: TaskID, advance: float = 1) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def rich_progress(
|
||||||
|
*columns: Union[str, ProgressColumn],
|
||||||
|
console: Optional[Console] = None,
|
||||||
|
auto_refresh: bool = True,
|
||||||
|
refresh_per_second: float = 10,
|
||||||
|
speed_estimate_period: float = 30.0,
|
||||||
|
transient: bool = False,
|
||||||
|
redirect_stdout: bool = True,
|
||||||
|
redirect_stderr: bool = True,
|
||||||
|
get_time: Optional[GetTimeCallable] = None,
|
||||||
|
disable: bool = False,
|
||||||
|
expand: bool = False,
|
||||||
|
mock: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Return a rich.progress.Progress object unless mock=True or os.getenv("OSXPHOTOS_IS_TESTING") is set"""
|
||||||
|
# if OSXPHOTOS_IS_TESTING is set or mock=True, return a MockProgress object
|
||||||
|
if mock or OSXPHOTOS_IS_TESTING:
|
||||||
|
return MockProgress()
|
||||||
|
return Progress(
|
||||||
|
*columns,
|
||||||
|
console=console,
|
||||||
|
auto_refresh=auto_refresh,
|
||||||
|
refresh_per_second=refresh_per_second,
|
||||||
|
speed_estimate_period=speed_estimate_period,
|
||||||
|
transient=transient,
|
||||||
|
redirect_stdout=redirect_stdout,
|
||||||
|
redirect_stderr=redirect_stderr,
|
||||||
|
get_time=get_time,
|
||||||
|
disable=disable,
|
||||||
|
expand=expand,
|
||||||
|
)
|
||||||
@@ -12,7 +12,8 @@ from rich.syntax import Syntax
|
|||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db, verbose_print
|
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
|
||||||
|
from .verbose import verbose_print
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="snap")
|
@click.command(name="snap")
|
||||||
|
|||||||
132
osxphotos/cli/theme.py
Normal file
132
osxphotos/cli/theme.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""theme command for osxphotos for managing color themes"""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich_theme_manager import Theme
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_click_echo
|
||||||
|
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
|
||||||
|
from .help import get_help_msg
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="theme")
|
||||||
|
@click.pass_obj
|
||||||
|
@click.pass_context
|
||||||
|
@click.option("--default", is_flag=True, help="Show default theme.")
|
||||||
|
@click.option("--list", "list_", is_flag=True, help="List all themes.")
|
||||||
|
@click.option(
|
||||||
|
"--config",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Print configuration for THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--preview",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Preview THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--edit",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Edit THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--clone",
|
||||||
|
metavar="THEME NEW_THEME",
|
||||||
|
nargs=2,
|
||||||
|
type=str,
|
||||||
|
help="Clone THEME to NEW_THEME.",
|
||||||
|
)
|
||||||
|
@click.option("--delete", metavar="THEME", help="Delete THEME.")
|
||||||
|
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
|
||||||
|
"""Manage osxphotos color themes."""
|
||||||
|
|
||||||
|
subcommands = [default, list_, config, preview, edit, clone, delete]
|
||||||
|
subcommand_names = (
|
||||||
|
"--default, --list, --config, --preview, --edit, --clone, --delete"
|
||||||
|
)
|
||||||
|
if not any(subcommands):
|
||||||
|
click.echo(
|
||||||
|
f"Must specify exactly one of: {subcommand_names}\n",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
rich_click_echo(get_help_msg(theme), err=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if sum(bool(cmd) for cmd in subcommands) != 1:
|
||||||
|
# only a single subcommand may be specified
|
||||||
|
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
|
||||||
|
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
console = Console(theme=get_default_theme())
|
||||||
|
|
||||||
|
if default:
|
||||||
|
default = get_default_theme()
|
||||||
|
theme_manager.list_themes(theme_names=[default.name])
|
||||||
|
return
|
||||||
|
|
||||||
|
if list_:
|
||||||
|
theme_manager.list_themes()
|
||||||
|
return
|
||||||
|
|
||||||
|
if config:
|
||||||
|
if config == "_DEFAULT_":
|
||||||
|
print(get_default_theme().config)
|
||||||
|
else:
|
||||||
|
print(get_theme(config).config)
|
||||||
|
return
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
|
||||||
|
theme_manager.preview_theme(theme_)
|
||||||
|
return
|
||||||
|
|
||||||
|
if edit:
|
||||||
|
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
|
||||||
|
config_file = pathlib.Path(theme_.path)
|
||||||
|
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
|
||||||
|
click.edit(filename=str(config_file))
|
||||||
|
return
|
||||||
|
|
||||||
|
if clone:
|
||||||
|
src_theme = get_theme(clone[0])
|
||||||
|
dest_path = get_theme_dir() / f"{clone[1]}.theme"
|
||||||
|
if dest_path.exists():
|
||||||
|
raise click.ClickException(
|
||||||
|
f"Theme '{clone[1]}' already exists at {dest_path}"
|
||||||
|
)
|
||||||
|
dest_theme = Theme(
|
||||||
|
name=clone[1],
|
||||||
|
description=src_theme.description,
|
||||||
|
inherit=src_theme.inherit,
|
||||||
|
tags=src_theme.tags,
|
||||||
|
styles={
|
||||||
|
style_name: src_theme.styles[style_name]
|
||||||
|
for style_name in src_theme.style_names
|
||||||
|
},
|
||||||
|
)
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
theme_manager.add(dest_theme)
|
||||||
|
theme_ = get_theme(dest_theme.name)
|
||||||
|
console.print(
|
||||||
|
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
|
||||||
|
f"at [filepath]{theme_.path}[/]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if delete:
|
||||||
|
theme_ = get_theme(delete)
|
||||||
|
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
|
||||||
|
theme_manager.remove(theme_)
|
||||||
|
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
|
||||||
|
return
|
||||||
143
osxphotos/cli/verbose.py
Normal file
143
osxphotos/cli/verbose.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""helper functions for printing verbose output"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import typing as t
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.theme import Theme
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_click_echo
|
||||||
|
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, time_stamp
|
||||||
|
|
||||||
|
# set to 1 if running tests
|
||||||
|
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
|
||||||
|
|
||||||
|
# include error/warning emoji's in verbose output
|
||||||
|
ERROR_EMOJI = True
|
||||||
|
|
||||||
|
__all__ = ["get_verbose_console", "verbose_print"]
|
||||||
|
|
||||||
|
|
||||||
|
class _Console:
|
||||||
|
"""Store console object for verbose output"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._console: t.Optional[Console] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def console(self):
|
||||||
|
return self._console
|
||||||
|
|
||||||
|
@console.setter
|
||||||
|
def console(self, console: Console):
|
||||||
|
self._console = console
|
||||||
|
|
||||||
|
|
||||||
|
_console = _Console()
|
||||||
|
|
||||||
|
|
||||||
|
def noop(*args, **kwargs):
|
||||||
|
"""no-op function"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_verbose_console() -> Console:
|
||||||
|
"""Get console object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Console object
|
||||||
|
"""
|
||||||
|
global _console
|
||||||
|
if _console.console is None:
|
||||||
|
_console.console = Console(force_terminal=True)
|
||||||
|
return _console.console
|
||||||
|
|
||||||
|
|
||||||
|
def verbose_print(
|
||||||
|
verbose: bool = True,
|
||||||
|
timestamp: bool = False,
|
||||||
|
rich: bool = False,
|
||||||
|
highlight: bool = False,
|
||||||
|
theme: t.Optional[Theme] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Callable:
|
||||||
|
"""Create verbose function to print output
|
||||||
|
|
||||||
|
Args:
|
||||||
|
verbose: if True, returns verbose print function otherwise returns no-op function
|
||||||
|
timestamp: if True, includes timestamp in verbose output
|
||||||
|
rich: use rich.print instead of click.echo
|
||||||
|
highlight: if True, use automatic rich.print highlighting
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
function to print output
|
||||||
|
"""
|
||||||
|
if not verbose:
|
||||||
|
return noop
|
||||||
|
|
||||||
|
global _console
|
||||||
|
_console.console = Console(theme=theme, width=10_000)
|
||||||
|
|
||||||
|
# closure to capture timestamp
|
||||||
|
def verbose_(*args):
|
||||||
|
"""print output if verbose flag set"""
|
||||||
|
styled_args = []
|
||||||
|
timestamp_str = f"{str(datetime.now())} -- " if timestamp else ""
|
||||||
|
for arg in args:
|
||||||
|
if type(arg) == str:
|
||||||
|
arg = timestamp_str + arg
|
||||||
|
if "error" in arg.lower():
|
||||||
|
arg = click.style(arg, fg=CLI_COLOR_ERROR)
|
||||||
|
elif "warning" in arg.lower():
|
||||||
|
arg = click.style(arg, fg=CLI_COLOR_WARNING)
|
||||||
|
styled_args.append(arg)
|
||||||
|
click.echo(*styled_args, **kwargs)
|
||||||
|
|
||||||
|
def rich_verbose_(*args):
|
||||||
|
"""rich.print output if verbose flag set"""
|
||||||
|
global ERROR_EMOJI
|
||||||
|
timestamp_str = time_stamp() if timestamp else ""
|
||||||
|
new_args = []
|
||||||
|
for arg in args:
|
||||||
|
if type(arg) == str:
|
||||||
|
if "error" in arg.lower():
|
||||||
|
arg = f"[error]{arg}"
|
||||||
|
if ERROR_EMOJI:
|
||||||
|
arg = f":cross_mark-emoji: {arg}"
|
||||||
|
elif "warning" in arg.lower():
|
||||||
|
arg = f"[warning]{arg}"
|
||||||
|
if ERROR_EMOJI:
|
||||||
|
arg = f":warning-emoji: {arg}"
|
||||||
|
arg = timestamp_str + arg
|
||||||
|
new_args.append(arg)
|
||||||
|
_console.console.print(*new_args, highlight=highlight, **kwargs)
|
||||||
|
|
||||||
|
def rich_verbose_testing_(*args):
|
||||||
|
"""print output if verbose flag set using rich.print"""
|
||||||
|
global ERROR_EMOJI
|
||||||
|
timestamp_str = time_stamp() if timestamp else ""
|
||||||
|
new_args = []
|
||||||
|
for arg in args:
|
||||||
|
if type(arg) == str:
|
||||||
|
if "error" in arg.lower():
|
||||||
|
arg = f"[error]{arg}"
|
||||||
|
if ERROR_EMOJI:
|
||||||
|
arg = f":cross_mark-emoji: {arg}"
|
||||||
|
elif "warning" in arg.lower():
|
||||||
|
arg = f"[warning]{arg}"
|
||||||
|
if ERROR_EMOJI:
|
||||||
|
arg = f":warning-emoji: {arg}"
|
||||||
|
arg = timestamp_str + arg
|
||||||
|
new_args.append(arg)
|
||||||
|
rich_click_echo(*new_args, theme=theme, **kwargs)
|
||||||
|
|
||||||
|
if rich and not OSXPHOTOS_IS_TESTING:
|
||||||
|
return rich_verbose_
|
||||||
|
elif rich:
|
||||||
|
return rich_verbose_testing_
|
||||||
|
else:
|
||||||
|
return verbose_
|
||||||
@@ -8,6 +8,16 @@ import traceback
|
|||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
|
from ._version import __version__
|
||||||
|
|
||||||
|
# store data to print out in crash log, set by set_crash_data
|
||||||
|
CRASH_DATA = {}
|
||||||
|
|
||||||
|
|
||||||
|
def set_crash_data(key_, data):
|
||||||
|
"""Set data to be printed in crash log"""
|
||||||
|
CRASH_DATA[key_] = data
|
||||||
|
|
||||||
|
|
||||||
def crash_reporter(filename, message, title, postamble, *extra_args):
|
def crash_reporter(filename, message, title, postamble, *extra_args):
|
||||||
"""Create a crash dump file on error named filename
|
"""Create a crash dump file on error named filename
|
||||||
@@ -30,9 +40,13 @@ def crash_reporter(filename, message, title, postamble, *extra_args):
|
|||||||
with open(filename, "w") as f:
|
with open(filename, "w") as f:
|
||||||
f.write(f"{title}\n")
|
f.write(f"{title}\n")
|
||||||
f.write(f"Created: {datetime.datetime.now()}\n")
|
f.write(f"Created: {datetime.datetime.now()}\n")
|
||||||
f.write(f"Python version: {sys.version}\n")
|
f.write(f"osxphotos version: {__version__}\n")
|
||||||
f.write(f"Platform: {platform.platform()}\n")
|
f.write(f"Platform: {platform.platform()}\n")
|
||||||
|
f.write(f"Python version: {sys.version}\n")
|
||||||
f.write(f"sys.argv: {sys.argv}\n")
|
f.write(f"sys.argv: {sys.argv}\n")
|
||||||
|
f.write("CRASH_DATA: \\n")
|
||||||
|
for k, v in CRASH_DATA.items():
|
||||||
|
f.write(f"{k}: {v}\n")
|
||||||
for arg in extra_args:
|
for arg in extra_args:
|
||||||
f.write(f"{arg}\n")
|
f.write(f"{arg}\n")
|
||||||
f.write(f"Error: {e}\n")
|
f.write(f"Error: {e}\n")
|
||||||
|
|||||||
103
osxphotos/debug.py
Normal file
103
osxphotos/debug.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Utilities for debugging"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import wrapt
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
# global variable to control debug output
|
||||||
|
# set via --debug
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
|
def set_debug(debug: bool):
|
||||||
|
"""set debug flag"""
|
||||||
|
global DEBUG
|
||||||
|
DEBUG = debug
|
||||||
|
logging.disable(logging.NOTSET if debug else logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def is_debug():
|
||||||
|
"""return debug flag"""
|
||||||
|
return DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
def debug_watch(wrapped, instance, args, kwargs):
|
||||||
|
"""For use with wrapt.wrap_function_wrapper to watch calls to a function"""
|
||||||
|
caller = sys._getframe().f_back.f_code.co_name
|
||||||
|
name = wrapped.__name__
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
print(
|
||||||
|
f"{timestamp} {name} called from {caller} with args: {args} and kwargs: {kwargs}"
|
||||||
|
)
|
||||||
|
start_t = time.perf_counter()
|
||||||
|
rv = wrapped(*args, **kwargs)
|
||||||
|
stop_t = time.perf_counter()
|
||||||
|
print(f"{timestamp} {name} returned: {rv}, elapsed time: {stop_t - start_t} sec")
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def debug_breakpoint(wrapped, instance, args, kwargs):
|
||||||
|
"""For use with wrapt.wrap_function_wrapper to set breakpoint on a function"""
|
||||||
|
breakpoint()
|
||||||
|
return wrapped(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_function(function_path, wrapper):
|
||||||
|
"""Wrap a function with wrapper function"""
|
||||||
|
module, name = function_path.split(".", 1)
|
||||||
|
try:
|
||||||
|
return wrapt.wrap_function_wrapper(module, name, wrapper)
|
||||||
|
except AttributeError as e:
|
||||||
|
raise AttributeError(f"{module}.{name} does not exist") from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_options(arg_names: List, argv: List) -> Dict:
|
||||||
|
"""Get the options for the debug options;
|
||||||
|
Some of the debug options like --watch and --breakpoint need to be processed before any other packages are loaded
|
||||||
|
so they can't be handled in the normal click argument processing, thus this function is called
|
||||||
|
from osxphotos/cli/__init__.py
|
||||||
|
|
||||||
|
Assumes multi-valued options are OK and that all options take form of --option VALUE or --option=VALUE
|
||||||
|
"""
|
||||||
|
# argv[0] is the program name
|
||||||
|
# argv[1] is the command
|
||||||
|
# argv[2:] are the arguments
|
||||||
|
args = {}
|
||||||
|
for arg_name in arg_names:
|
||||||
|
for idx, arg in enumerate(argv[1:]):
|
||||||
|
if arg.startswith(f"{arg_name}="):
|
||||||
|
arg_value = arg.split("=")[1]
|
||||||
|
try:
|
||||||
|
args[arg].append(arg_value)
|
||||||
|
except KeyError:
|
||||||
|
args[arg] = [arg_value]
|
||||||
|
elif arg == arg_name:
|
||||||
|
try:
|
||||||
|
args[arg].append(argv[idx + 2])
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
args[arg] = [argv[idx + 2]]
|
||||||
|
except IndexError as e:
|
||||||
|
raise ValueError(f"Missing value for {arg}") from e
|
||||||
|
except IndexError as e:
|
||||||
|
raise ValueError(f"Missing value for {arg}") from e
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_flags(arg_names: List, argv: List) -> Dict:
|
||||||
|
"""Get the flags for the debug options;
|
||||||
|
Processes flags like --debug that resolve to True or False
|
||||||
|
"""
|
||||||
|
# argv[0] is the program name
|
||||||
|
# argv[1] is the command
|
||||||
|
# argv[2:] are the arguments
|
||||||
|
args = {arg_name: False for arg_name in arg_names}
|
||||||
|
for arg_name in arg_names:
|
||||||
|
if arg_name in argv[1:]:
|
||||||
|
args[arg_name] = True
|
||||||
|
return args
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import tempfile
|
||||||
import sys
|
import typing as t
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -67,6 +68,13 @@ class FileUtilABC(ABC):
|
|||||||
def rename(cls, src, dest):
|
def rename(cls, src, dest):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def tmpdir(
|
||||||
|
cls, prefix: t.Optional[str] = None, dir: t.Optional[str] = None
|
||||||
|
) -> tempfile.TemporaryDirectory:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileUtilMacOS(FileUtilABC):
|
class FileUtilMacOS(FileUtilABC):
|
||||||
"""Various file utilities"""
|
"""Various file utilities"""
|
||||||
@@ -84,11 +92,10 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
if not os.path.isfile(src):
|
if not os.path.isfile(src):
|
||||||
raise FileNotFoundError("src file does not appear to exist", src)
|
raise FileNotFoundError("src file does not appear to exist", src)
|
||||||
|
|
||||||
# if error on copy, subprocess will raise CalledProcessError
|
|
||||||
try:
|
try:
|
||||||
os.link(src, dest)
|
os.link(src, dest)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e from e
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, src, dest):
|
def copy(cls, src, dest):
|
||||||
@@ -222,6 +229,17 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
os.rename(str(src), str(dest))
|
os.rename(str(src), str(dest))
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tmpdir(
|
||||||
|
cls, prefix: t.Optional[str] = None, dir: t.Optional[str] = None
|
||||||
|
) -> tempfile.TemporaryDirectory:
|
||||||
|
"""Securely creates a temporary directory using the same rules as mkdtemp().
|
||||||
|
The resulting object can be used as a context manager.
|
||||||
|
On completion of the context or destruction of the temporary directory object,
|
||||||
|
the newly created temporary directory and all its contents are removed from the filesystem.
|
||||||
|
"""
|
||||||
|
return TemporaryDirectory(prefix=prefix, dir=dir)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sig(st):
|
def _sig(st):
|
||||||
"""return tuple of (mode, size, mtime) of file based on os.stat
|
"""return tuple of (mode, size, mtime) of file based on os.stat
|
||||||
@@ -240,7 +258,7 @@ class FileUtil(FileUtilMacOS):
|
|||||||
|
|
||||||
class FileUtilNoOp(FileUtil):
|
class FileUtilNoOp(FileUtil):
|
||||||
"""No-Op implementation of FileUtil for testing / dry-run mode
|
"""No-Op implementation of FileUtil for testing / dry-run mode
|
||||||
all methods with exception of cmp, cmp_file_sig and file_cmp are no-op
|
all methods with exception of tmpdir, cmp, cmp_file_sig and file_cmp are no-op
|
||||||
cmp and cmp_file_sig functions as FileUtil methods do
|
cmp and cmp_file_sig functions as FileUtil methods do
|
||||||
file_cmp returns mock data
|
file_cmp returns mock data
|
||||||
"""
|
"""
|
||||||
@@ -249,8 +267,6 @@ class FileUtilNoOp(FileUtil):
|
|||||||
def noop(*args):
|
def noop(*args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
verbose = noop
|
|
||||||
|
|
||||||
def __new__(cls, verbose=None):
|
def __new__(cls, verbose=None):
|
||||||
if verbose:
|
if verbose:
|
||||||
if callable(verbose):
|
if callable(verbose):
|
||||||
@@ -261,33 +277,43 @@ class FileUtilNoOp(FileUtil):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def hardlink(cls, src, dest):
|
def hardlink(cls, src, dest):
|
||||||
cls.verbose(f"hardlink: {src} {dest}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, src, dest, norsrc=False):
|
def copy(cls, src, dest, norsrc=False):
|
||||||
cls.verbose(f"copy: {src} {dest}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unlink(cls, dest):
|
def unlink(cls, dest):
|
||||||
cls.verbose(f"unlink: {dest}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rmdir(cls, dest):
|
def rmdir(cls, dest):
|
||||||
cls.verbose(f"rmdir: {dest}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def utime(cls, path, times):
|
def utime(cls, path, times):
|
||||||
cls.verbose(f"utime: {path}, {times}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def file_sig(cls, file1):
|
def file_sig(cls, file1):
|
||||||
cls.verbose(f"file_sig: {file1}")
|
|
||||||
return (42, 42, 42)
|
return (42, 42, 42)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
||||||
cls.verbose(f"convert_to_jpeg: {src_file}, {dest_file}, {compression_quality}")
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rename(cls, src, dest):
|
def rename(cls, src, dest):
|
||||||
cls.verbose(f"rename: {src}, {dest}")
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tmpdir(
|
||||||
|
cls, prefix: t.Optional[str] = None, dir: t.Optional[str] = None
|
||||||
|
) -> tempfile.TemporaryDirectory:
|
||||||
|
"""Securely creates a temporary directory using the same rules as mkdtemp().
|
||||||
|
The resulting object can be used as a context manager.
|
||||||
|
On completion of the context or destruction of the temporary directory object,
|
||||||
|
the newly created temporary directory and all its contents are removed from the filesystem.
|
||||||
|
"""
|
||||||
|
return TemporaryDirectory(prefix=prefix, dir=dir)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
""" PhotoExport class to export photos
|
""" PhotoExport class to export photos
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@@ -10,9 +9,10 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import typing as t
|
||||||
from collections import namedtuple # pylint: disable=syntax-error
|
from collections import namedtuple # pylint: disable=syntax-error
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
|
from enum import Enum
|
||||||
|
|
||||||
import photoscript
|
import photoscript
|
||||||
from mako.template import Template
|
from mako.template import Template
|
||||||
@@ -43,6 +43,7 @@ from .photokit import (
|
|||||||
PhotoLibrary,
|
PhotoLibrary,
|
||||||
)
|
)
|
||||||
from .phototemplate import RenderOptions
|
from .phototemplate import RenderOptions
|
||||||
|
from .rich_utils import add_rich_markup_tag
|
||||||
from .uti import get_preferred_uti_extension
|
from .uti import get_preferred_uti_extension
|
||||||
from .utils import increment_filename, lineno, list_directory
|
from .utils import increment_filename, lineno, list_directory
|
||||||
|
|
||||||
@@ -55,12 +56,23 @@ __all__ = [
|
|||||||
"rename_jpeg_files",
|
"rename_jpeg_files",
|
||||||
]
|
]
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
|
|
||||||
# retry if download_missing/use_photos_export fails the first time (which sometimes it does)
|
# retry if download_missing/use_photos_export fails the first time (which sometimes it does)
|
||||||
MAX_PHOTOSCRIPT_RETRIES = 3
|
MAX_PHOTOSCRIPT_RETRIES = 3
|
||||||
|
|
||||||
|
# return values for _should_update_photo
|
||||||
|
class ShouldUpdate(Enum):
|
||||||
|
NOT_IN_DATABASE = 1
|
||||||
|
HARDLINK_DIFFERENT_FILES = 2
|
||||||
|
NOT_HARDLINK_SAME_FILES = 3
|
||||||
|
DEST_SIG_DIFFERENT = 4
|
||||||
|
EXPORT_OPTIONS_DIFFERENT = 5
|
||||||
|
EXIFTOOL_DIFFERENT = 6
|
||||||
|
EDITED_SIG_DIFFERENT = 7
|
||||||
|
DIGEST_DIFFERENT = 8
|
||||||
|
|
||||||
|
|
||||||
class ExportError(Exception):
|
class ExportError(Exception):
|
||||||
"""error during export"""
|
"""error during export"""
|
||||||
@@ -74,11 +86,11 @@ class ExportOptions:
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
convert_to_jpeg (bool): if True, converts non-jpeg images to jpeg
|
convert_to_jpeg (bool): if True, converts non-jpeg images to jpeg
|
||||||
description_template (str): optional template string that will be rendered for use as photo description
|
description_template (str): t.Optional template string that will be rendered for use as photo description
|
||||||
download_missing: (bool, default=False): if True will attempt to export photo via applescript interaction with Photos if missing (see also use_photokit, use_photos_export)
|
download_missing: (bool, default=False): if True will attempt to export photo via applescript interaction with Photos if missing (see also use_photokit, use_photos_export)
|
||||||
dry_run: (bool, default=False): set to True to run in "dry run" mode
|
dry_run: (bool, default=False): set to True to run in "dry run" mode
|
||||||
edited: (bool, default=False): if True will export the edited version of the photo otherwise exports the original version
|
edited: (bool, default=False): if True will export the edited version of the photo otherwise exports the original version
|
||||||
exiftool_flags (list of str): optional list of flags to pass to exiftool when using exiftool option, e.g ["-m", "-F"]
|
exiftool_flags (list of str): t.Optional list of flags to pass to exiftool when using exiftool option, e.g ["-m", "-F"]
|
||||||
exiftool: (bool, default = False): if True, will use exiftool to write metadata to export file
|
exiftool: (bool, default = False): if True, will use exiftool to write metadata to export file
|
||||||
export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
|
export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
|
||||||
export_db: (ExportDB): instance of a class that conforms to ExportDB with methods for getting/setting data related to exported files to compare update state
|
export_db: (ExportDB): instance of a class that conforms to ExportDB with methods for getting/setting data related to exported files to compare update state
|
||||||
@@ -97,11 +109,12 @@ class ExportOptions:
|
|||||||
merge_exif_persons (bool): if True, merged persons found in file's exif data (requires exiftool)
|
merge_exif_persons (bool): if True, merged persons found in file's exif data (requires exiftool)
|
||||||
overwrite (bool, default=False): if True will overwrite files if they already exist
|
overwrite (bool, default=False): if True will overwrite files if they already exist
|
||||||
persons (bool): if True, include persons in exported metadata
|
persons (bool): if True, include persons in exported metadata
|
||||||
preview_suffix (str): optional string to append to end of filename for preview images
|
preview_suffix (str): t.Optional string to append to end of filename for preview images
|
||||||
preview (bool): if True, also exports preview image
|
preview (bool): if True, also exports preview image
|
||||||
raw_photo (bool, default=False): if True, will also export the associated RAW photo
|
raw_photo (bool, default=False): if True, will also export the associated RAW photo
|
||||||
render_options (RenderOptions): optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
render_options (RenderOptions): t.Optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
||||||
replace_keywords (bool): if True, keyword_template replaces any keywords, otherwise it's additive
|
replace_keywords (bool): if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
rich (bool): if True, will use rich markup with verbose output
|
||||||
sidecar_drop_ext (bool, default=False): if True, drops the photo's extension from sidecar filename (e.g. 'IMG_1234.json' instead of 'IMG_1234.JPG.json')
|
sidecar_drop_ext (bool, default=False): if True, drops the photo's extension from sidecar filename (e.g. 'IMG_1234.json' instead of 'IMG_1234.JPG.json')
|
||||||
sidecar: bit field (int): set to one or more of SIDECAR_XMP, SIDECAR_JSON, SIDECAR_EXIFTOOL
|
sidecar: bit field (int): set to one or more of SIDECAR_XMP, SIDECAR_JSON, SIDECAR_EXIFTOOL
|
||||||
- SIDECAR_JSON: if set will write a json sidecar with data in format readable by exiftool sidecar filename will be dest/filename.json;
|
- SIDECAR_JSON: if set will write a json sidecar with data in format readable by exiftool sidecar filename will be dest/filename.json;
|
||||||
@@ -117,27 +130,28 @@ class ExportOptions:
|
|||||||
use_persons_as_keywords (bool, default = False): if True, will include person names in keywords when exporting metadata with exiftool or sidecar
|
use_persons_as_keywords (bool, default = False): if True, will include person names in keywords when exporting metadata with exiftool or sidecar
|
||||||
use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos even if not missing (see also use_photokit, download_missing)
|
use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos even if not missing (see also use_photokit, download_missing)
|
||||||
use_photokit (bool, default=False): if True, will use photokit to export photos when use_photos_export is True
|
use_photokit (bool, default=False): if True, will use photokit to export photos when use_photos_export is True
|
||||||
verbose (Callable): optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
verbose (callable): optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||||
|
tmpdir: (str, default=None): Optional directory to use for temporary files, if None (default) uses system tmp directory
|
||||||
"""
|
"""
|
||||||
|
|
||||||
convert_to_jpeg: bool = False
|
convert_to_jpeg: bool = False
|
||||||
description_template: Optional[str] = None
|
description_template: t.Optional[str] = None
|
||||||
download_missing: bool = False
|
download_missing: bool = False
|
||||||
dry_run: bool = False
|
dry_run: bool = False
|
||||||
edited: bool = False
|
edited: bool = False
|
||||||
exiftool_flags: Optional[List] = None
|
exiftool_flags: t.Optional[t.List] = None
|
||||||
exiftool: bool = False
|
exiftool: bool = False
|
||||||
export_as_hardlink: bool = False
|
export_as_hardlink: bool = False
|
||||||
export_db: Optional[ExportDB] = None
|
export_db: t.Optional[ExportDB] = None
|
||||||
face_regions: bool = True
|
face_regions: bool = True
|
||||||
fileutil: Optional[FileUtil] = None
|
fileutil: t.Optional[FileUtil] = None
|
||||||
force_update: bool = False
|
force_update: bool = False
|
||||||
ignore_date_modified: bool = False
|
ignore_date_modified: bool = False
|
||||||
ignore_signature: bool = False
|
ignore_signature: bool = False
|
||||||
increment: bool = True
|
increment: bool = True
|
||||||
jpeg_ext: Optional[str] = None
|
jpeg_ext: t.Optional[str] = None
|
||||||
jpeg_quality: float = 1.0
|
jpeg_quality: float = 1.0
|
||||||
keyword_template: Optional[List[str]] = None
|
keyword_template: t.Optional[t.List[str]] = None
|
||||||
live_photo: bool = False
|
live_photo: bool = False
|
||||||
location: bool = True
|
location: bool = True
|
||||||
merge_exif_keywords: bool = False
|
merge_exif_keywords: bool = False
|
||||||
@@ -147,8 +161,9 @@ class ExportOptions:
|
|||||||
preview_suffix: str = DEFAULT_PREVIEW_SUFFIX
|
preview_suffix: str = DEFAULT_PREVIEW_SUFFIX
|
||||||
preview: bool = False
|
preview: bool = False
|
||||||
raw_photo: bool = False
|
raw_photo: bool = False
|
||||||
render_options: Optional[RenderOptions] = None
|
render_options: t.Optional[RenderOptions] = None
|
||||||
replace_keywords: bool = False
|
replace_keywords: bool = False
|
||||||
|
rich: bool = False
|
||||||
sidecar_drop_ext: bool = False
|
sidecar_drop_ext: bool = False
|
||||||
sidecar: int = 0
|
sidecar: int = 0
|
||||||
strip: bool = False
|
strip: bool = False
|
||||||
@@ -159,7 +174,8 @@ class ExportOptions:
|
|||||||
use_persons_as_keywords: bool = False
|
use_persons_as_keywords: bool = False
|
||||||
use_photokit: bool = False
|
use_photokit: bool = False
|
||||||
use_photos_export: bool = False
|
use_photos_export: bool = False
|
||||||
verbose: Optional[Callable] = None
|
verbose: t.Optional[t.Callable] = None
|
||||||
|
tmpdir: t.Optional[str] = None
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
@@ -176,13 +192,13 @@ class StagedFiles:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
original: Optional[str] = None,
|
original: t.Optional[str] = None,
|
||||||
original_live: Optional[str] = None,
|
original_live: t.Optional[str] = None,
|
||||||
edited: Optional[str] = None,
|
edited: t.Optional[str] = None,
|
||||||
edited_live: Optional[str] = None,
|
edited_live: t.Optional[str] = None,
|
||||||
preview: Optional[str] = None,
|
preview: t.Optional[str] = None,
|
||||||
raw: Optional[str] = None,
|
raw: t.Optional[str] = None,
|
||||||
error: Optional[List[str]] = None,
|
error: t.Optional[t.List[str]] = None,
|
||||||
):
|
):
|
||||||
self.original = original
|
self.original = original
|
||||||
self.original_live = original_live
|
self.original_live = original_live
|
||||||
@@ -359,23 +375,27 @@ class ExportResults:
|
|||||||
|
|
||||||
|
|
||||||
class PhotoExporter:
|
class PhotoExporter:
|
||||||
def __init__(self, photo: "PhotoInfo"):
|
def __init__(self, photo: "PhotoInfo", tmpdir: t.Optional[str] = None):
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self._render_options = RenderOptions()
|
self._render_options = RenderOptions()
|
||||||
self._verbose = self.photo._verbose
|
self._verbose = self.photo._verbose
|
||||||
|
|
||||||
|
# define functions for adding markup
|
||||||
|
self._filepath = add_rich_markup_tag("filepath", rich=False)
|
||||||
|
self._filename = add_rich_markup_tag("filename", rich=False)
|
||||||
|
self._uuid = add_rich_markup_tag("uuid", rich=False)
|
||||||
|
self._num = add_rich_markup_tag("num", rich=False)
|
||||||
|
|
||||||
# temp directory for staging downloaded missing files
|
# temp directory for staging downloaded missing files
|
||||||
self._temp_dir = tempfile.TemporaryDirectory(
|
self._temp_dir = None
|
||||||
prefix=f"osxphotos_photo_exporter_{self.photo.uuid}_"
|
self._temp_dir_path = None
|
||||||
)
|
|
||||||
self._temp_dir_path = pathlib.Path(self._temp_dir.name)
|
|
||||||
self.fileutil = FileUtil
|
self.fileutil = FileUtil
|
||||||
|
|
||||||
def export(
|
def export(
|
||||||
self,
|
self,
|
||||||
dest,
|
dest,
|
||||||
filename=None,
|
filename=None,
|
||||||
options: Optional[ExportOptions] = None,
|
options: t.Optional[ExportOptions] = None,
|
||||||
) -> ExportResults:
|
) -> ExportResults:
|
||||||
"""export photo
|
"""export photo
|
||||||
|
|
||||||
@@ -389,7 +409,7 @@ class PhotoExporter:
|
|||||||
in which case export will use the extension provided by Photos upon export.
|
in which case export will use the extension provided by Photos upon export.
|
||||||
e.g. to get the extension of the edited photo,
|
e.g. to get the extension of the edited photo,
|
||||||
reference PhotoInfo.path_edited
|
reference PhotoInfo.path_edited
|
||||||
options (ExportOptions): optional ExportOptions instance
|
options (ExportOptions): t.Optional ExportOptions instance
|
||||||
|
|
||||||
Returns: ExportResults instance
|
Returns: ExportResults instance
|
||||||
|
|
||||||
@@ -399,10 +419,19 @@ class PhotoExporter:
|
|||||||
|
|
||||||
options = options or ExportOptions()
|
options = options or ExportOptions()
|
||||||
|
|
||||||
|
# temp dir must be initialized before any of the methods called by export() are called
|
||||||
|
self._init_temp_dir(options)
|
||||||
|
|
||||||
verbose = options.verbose or self._verbose
|
verbose = options.verbose or self._verbose
|
||||||
if verbose and not callable(verbose):
|
if verbose and not callable(verbose):
|
||||||
raise TypeError("verbose must be callable")
|
raise TypeError("verbose must be callable")
|
||||||
|
|
||||||
|
# define functions for adding markup
|
||||||
|
self._filepath = add_rich_markup_tag("filepath", rich=options.rich)
|
||||||
|
self._filename = add_rich_markup_tag("filename", rich=options.rich)
|
||||||
|
self._uuid = add_rich_markup_tag("uuid", rich=options.rich)
|
||||||
|
self._num = add_rich_markup_tag("num", rich=options.rich)
|
||||||
|
|
||||||
# can't use export_as_hardlink with download_missing, use_photos_export as can't hardlink the temporary files downloaded
|
# can't use export_as_hardlink with download_missing, use_photos_export as can't hardlink the temporary files downloaded
|
||||||
if options.export_as_hardlink and options.download_missing:
|
if options.export_as_hardlink and options.download_missing:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -462,7 +491,7 @@ class PhotoExporter:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing {'edited' if options.edited else 'original'} photo {self.photo.original_filename} ({self.photo.uuid})"
|
f"Skipping missing {'edited' if options.edited else 'original'} photo {self._filename(self.photo.original_filename)} ({self._uuid(self.photo.uuid)})"
|
||||||
)
|
)
|
||||||
all_results.missing.append(dest)
|
all_results.missing.append(dest)
|
||||||
|
|
||||||
@@ -479,7 +508,7 @@ class PhotoExporter:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing live photo for {self.photo.original_filename} ({self.photo.uuid})"
|
f"Skipping missing live photo for {self._filename(self.photo.original_filename)} ({self._uuid(self.photo.uuid)})"
|
||||||
)
|
)
|
||||||
all_results.missing.append(live_name)
|
all_results.missing.append(live_name)
|
||||||
|
|
||||||
@@ -495,7 +524,7 @@ class PhotoExporter:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing edited live photo for {self.photo.original_filename} ({self.photo.uuid})"
|
f"Skipping missing edited live photo for {self._filename(self.photo.original_filename)} ({self._uuid(self.photo.uuid)})"
|
||||||
)
|
)
|
||||||
all_results.missing.append(live_name)
|
all_results.missing.append(live_name)
|
||||||
|
|
||||||
@@ -516,7 +545,7 @@ class PhotoExporter:
|
|||||||
raw_name = dest.parent / f"{dest.stem}.{raw_ext}"
|
raw_name = dest.parent / f"{dest.stem}.{raw_ext}"
|
||||||
all_results.missing.append(raw_name)
|
all_results.missing.append(raw_name)
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing raw photo for {self.photo.original_filename} ({self.photo.uuid})"
|
f"Skipping missing raw photo for {self._filename(self.photo.original_filename)} ({self._uuid(self.photo.uuid)})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# copy preview image if requested
|
# copy preview image if requested
|
||||||
@@ -547,14 +576,30 @@ class PhotoExporter:
|
|||||||
preview_name = dest.parent / f"{dest.stem}{options.preview_suffix}.jpeg"
|
preview_name = dest.parent / f"{dest.stem}{options.preview_suffix}.jpeg"
|
||||||
all_results.missing.append(preview_name)
|
all_results.missing.append(preview_name)
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing preview photo for {self.photo.original_filename} ({self.photo.uuid})"
|
f"Skipping missing preview photo for {self._filename(self.photo.original_filename)} ({self._uuid(self.photo.uuid)})"
|
||||||
)
|
)
|
||||||
|
|
||||||
all_results += self._write_sidecar_files(dest=dest, options=options)
|
all_results += self._write_sidecar_files(dest=dest, options=options)
|
||||||
|
|
||||||
return all_results
|
return all_results
|
||||||
|
|
||||||
def _touch_files(self, touch_files: List, options: ExportOptions) -> ExportResults:
|
def _init_temp_dir(self, options: ExportOptions):
|
||||||
|
"""Initialize (if necessary) the object's temporary directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: ExportOptions object
|
||||||
|
"""
|
||||||
|
if self._temp_dir is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
fileutil = options.fileutil or FileUtil
|
||||||
|
self._temp_dir = fileutil.tmpdir(prefix="osxphotos_export_", dir=options.tmpdir)
|
||||||
|
self._temp_dir_path = pathlib.Path(self._temp_dir.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _touch_files(
|
||||||
|
self, touch_files: t.List, options: ExportOptions
|
||||||
|
) -> ExportResults:
|
||||||
"""touch file date/time to match photo creation date/time; only touches files if needed"""
|
"""touch file date/time to match photo creation date/time; only touches files if needed"""
|
||||||
fileutil = options.fileutil
|
fileutil = options.fileutil
|
||||||
touch_results = []
|
touch_results = []
|
||||||
@@ -646,7 +691,7 @@ class PhotoExporter:
|
|||||||
|
|
||||||
def _should_update_photo(
|
def _should_update_photo(
|
||||||
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
||||||
) -> bool:
|
) -> t.Literal[True, False]:
|
||||||
"""Return True if photo should be updated, else False"""
|
"""Return True if photo should be updated, else False"""
|
||||||
export_db = options.export_db
|
export_db = options.export_db
|
||||||
fileutil = options.fileutil
|
fileutil = options.fileutil
|
||||||
@@ -655,42 +700,45 @@ class PhotoExporter:
|
|||||||
|
|
||||||
if not file_record:
|
if not file_record:
|
||||||
# photo doesn't exist in database, should update
|
# photo doesn't exist in database, should update
|
||||||
return True
|
return ShouldUpdate.NOT_IN_DATABASE
|
||||||
|
|
||||||
if options.export_as_hardlink and not dest.samefile(src):
|
if options.export_as_hardlink and not dest.samefile(src):
|
||||||
# different files, should update
|
# different files, should update
|
||||||
return True
|
return ShouldUpdate.HARDLINK_DIFFERENT_FILES
|
||||||
|
|
||||||
if not options.export_as_hardlink and dest.samefile(src):
|
if not options.export_as_hardlink and dest.samefile(src):
|
||||||
# same file but not exporting as hardlink, should update
|
# same file but not exporting as hardlink, should update
|
||||||
return True
|
return ShouldUpdate.NOT_HARDLINK_SAME_FILES
|
||||||
|
|
||||||
if not options.ignore_signature and not fileutil.cmp_file_sig(
|
if not options.ignore_signature and not fileutil.cmp_file_sig(
|
||||||
dest, file_record.dest_sig
|
dest, file_record.dest_sig
|
||||||
):
|
):
|
||||||
# destination file doesn't match what was last exported
|
# destination file doesn't match what was last exported
|
||||||
return True
|
return ShouldUpdate.DEST_SIG_DIFFERENT
|
||||||
|
|
||||||
if file_record.export_options != options.bit_flags:
|
if file_record.export_options != options.bit_flags:
|
||||||
# exporting with different set of options (e.g. exiftool), should update
|
# exporting with different set of options (e.g. exiftool), should update
|
||||||
# need to check this before exiftool in case exiftool options are different
|
# need to check this before exiftool in case exiftool options are different
|
||||||
# and export database is missing; this will always be True if database is missing
|
# and export database is missing; this will always be True if database is missing
|
||||||
# as it'll be None and bit_flags will be an int
|
# as it'll be None and bit_flags will be an int
|
||||||
return True
|
return ShouldUpdate.EXPORT_OPTIONS_DIFFERENT
|
||||||
|
|
||||||
if options.exiftool:
|
if options.exiftool:
|
||||||
current_exifdata = self._exiftool_json_sidecar(options=options)
|
current_exifdata = self._exiftool_json_sidecar(options=options)
|
||||||
return current_exifdata != file_record.exifdata
|
rv = current_exifdata != file_record.exifdata
|
||||||
|
# if using exiftool, don't need to continue checking edited below
|
||||||
|
# as exiftool will be used to update edited file
|
||||||
|
return ShouldUpdate.EXIFTOOL_DIFFERENT if rv else False
|
||||||
|
|
||||||
if options.edited and not fileutil.cmp_file_sig(src, file_record.src_sig):
|
if options.edited and not fileutil.cmp_file_sig(src, file_record.src_sig):
|
||||||
# edited file in Photos doesn't match what was last exported
|
# edited file in Photos doesn't match what was last exported
|
||||||
return True
|
return ShouldUpdate.EDITED_SIG_DIFFERENT
|
||||||
|
|
||||||
if options.force_update:
|
if options.force_update:
|
||||||
current_digest = hexdigest(self.photo.json())
|
current_digest = hexdigest(self.photo.json())
|
||||||
if current_digest != file_record.digest:
|
if current_digest != file_record.digest:
|
||||||
# metadata in Photos changed, force update
|
# metadata in Photos changed, force update
|
||||||
return True
|
return ShouldUpdate.DIGEST_DIFFERENT
|
||||||
|
|
||||||
# photo should not be updated
|
# photo should not be updated
|
||||||
return False
|
return False
|
||||||
@@ -731,21 +779,6 @@ class PhotoExporter:
|
|||||||
if options.live_photo and self.photo.live_photo:
|
if options.live_photo and self.photo.live_photo:
|
||||||
staged.edited_live = self.photo.path_edited_live_photo
|
staged.edited_live = self.photo.path_edited_live_photo
|
||||||
|
|
||||||
if options.exiftool and not options.dry_run and not options.export_as_hardlink:
|
|
||||||
# copy files to temp dir for exiftool to process before export
|
|
||||||
# not needed for download_missing or use_photokit as those files already staged to temp dir
|
|
||||||
for file_type in [
|
|
||||||
"raw",
|
|
||||||
"preview",
|
|
||||||
"original",
|
|
||||||
"original_live",
|
|
||||||
"edited",
|
|
||||||
"edited_live",
|
|
||||||
]:
|
|
||||||
staged_file = getattr(staged, file_type)
|
|
||||||
if staged_file:
|
|
||||||
setattr(staged, file_type, self._copy_to_temp_file(staged_file))
|
|
||||||
|
|
||||||
# download any missing files
|
# download any missing files
|
||||||
if options.download_missing:
|
if options.download_missing:
|
||||||
live_photo = staged.edited_live if options.edited else staged.original_live
|
live_photo = staged.edited_live if options.edited else staged.original_live
|
||||||
@@ -904,7 +937,7 @@ class PhotoExporter:
|
|||||||
results = StagedFiles()
|
results = StagedFiles()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exported = _export_photo_uuid_applescript(
|
exported = self._export_photo_uuid_applescript(
|
||||||
self.photo.uuid,
|
self.photo.uuid,
|
||||||
dest.parent,
|
dest.parent,
|
||||||
filestem=dest.stem,
|
filestem=dest.stem,
|
||||||
@@ -955,7 +988,7 @@ class PhotoExporter:
|
|||||||
|
|
||||||
def _should_convert_to_jpeg(
|
def _should_convert_to_jpeg(
|
||||||
self, dest: pathlib.Path, options: ExportOptions
|
self, dest: pathlib.Path, options: ExportOptions
|
||||||
) -> Tuple[pathlib.Path, ExportOptions]:
|
) -> t.Tuple[pathlib.Path, ExportOptions]:
|
||||||
"""Determine if a file really should be converted to jpeg or not
|
"""Determine if a file really should be converted to jpeg or not
|
||||||
and return the new destination and ExportOptions instance with the convert_to_jpeg flag set appropriately
|
and return the new destination and ExportOptions instance with the convert_to_jpeg flag set appropriately
|
||||||
"""
|
"""
|
||||||
@@ -1090,6 +1123,15 @@ class PhotoExporter:
|
|||||||
|
|
||||||
if options.exiftool:
|
if options.exiftool:
|
||||||
# if exiftool, write the metadata
|
# if exiftool, write the metadata
|
||||||
|
# need to copy the file to a temp file before writing metadata
|
||||||
|
src = pathlib.Path(src)
|
||||||
|
tmp_file = increment_filename(
|
||||||
|
self._temp_dir_path / f"{src.stem}_exiftool{src.suffix}"
|
||||||
|
)
|
||||||
|
fileutil.copy(src, tmp_file)
|
||||||
|
# point src to the tmp_file so that the original source is not modified
|
||||||
|
# and the export grabs the new file
|
||||||
|
src = tmp_file
|
||||||
exif_results = self._write_exif_metadata_to_file(
|
exif_results = self._write_exif_metadata_to_file(
|
||||||
src, dest, options=options
|
src, dest, options=options
|
||||||
)
|
)
|
||||||
@@ -1138,6 +1180,109 @@ class PhotoExporter:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def _export_photo_uuid_applescript(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
dest: str,
|
||||||
|
filestem=None,
|
||||||
|
original=True,
|
||||||
|
edited=False,
|
||||||
|
live_photo=False,
|
||||||
|
timeout=120,
|
||||||
|
burst=False,
|
||||||
|
dry_run=False,
|
||||||
|
overwrite=False,
|
||||||
|
):
|
||||||
|
"""Export photo to dest path using applescript to control Photos
|
||||||
|
If photo is a live photo, exports both the photo and associated .mov file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid: UUID of photo to export
|
||||||
|
dest: destination path to export to
|
||||||
|
filestem: (string) if provided, exported filename will be named stem.ext
|
||||||
|
where ext is extension of the file exported by photos (e.g. .jpeg, .mov, etc)
|
||||||
|
If not provided, file will be named with whatever name Photos uses
|
||||||
|
If filestem.ext exists, it wil be overwritten
|
||||||
|
original: (boolean) if True, export original image; default = True
|
||||||
|
edited: (boolean) if True, export edited photo; default = False
|
||||||
|
If photo not edited and edited=True, will still export the original image
|
||||||
|
caller must verify image has been edited
|
||||||
|
*Note*: must be called with either edited or original but not both,
|
||||||
|
will raise error if called with both edited and original = True
|
||||||
|
live_photo: (boolean) if True, export associated .mov live photo; default = False
|
||||||
|
timeout: timeout value in seconds; export will fail if applescript run time exceeds timeout
|
||||||
|
burst: (boolean) set to True if file is a burst image to avoid Photos export error
|
||||||
|
dry_run: (boolean) set to True to run in "dry run" mode which will download file but not actually copy to destination
|
||||||
|
|
||||||
|
Returns: list of paths to exported file(s) or None if export failed
|
||||||
|
|
||||||
|
Raises: ExportError if error during export
|
||||||
|
|
||||||
|
Note: For Live Photos, if edited=True, will export a jpeg but not the movie, even if photo
|
||||||
|
has not been edited. This is due to how Photos Applescript interface works.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dest = pathlib.Path(dest)
|
||||||
|
if not dest.is_dir():
|
||||||
|
raise ValueError(f"dest {dest} must be a directory")
|
||||||
|
|
||||||
|
if not original ^ edited:
|
||||||
|
raise ValueError("edited or original must be True but not both")
|
||||||
|
|
||||||
|
# export to a subdirectory of tmpdir
|
||||||
|
tmpdir = self.fileutil.tmpdir(
|
||||||
|
"osxphotos_applescript_export_", dir=self._temp_dir_path
|
||||||
|
)
|
||||||
|
|
||||||
|
exported_files = []
|
||||||
|
filename = None
|
||||||
|
try:
|
||||||
|
# I've seen intermittent failures with the PhotoScript export so retry if
|
||||||
|
# export doesn't return anything
|
||||||
|
retries = 0
|
||||||
|
while not exported_files and retries < MAX_PHOTOSCRIPT_RETRIES:
|
||||||
|
photo = photoscript.Photo(uuid)
|
||||||
|
filename = photo.filename
|
||||||
|
exported_files = photo.export(
|
||||||
|
tmpdir.name, original=original, timeout=timeout
|
||||||
|
)
|
||||||
|
retries += 1
|
||||||
|
except Exception as e:
|
||||||
|
raise ExportError(e)
|
||||||
|
|
||||||
|
if not exported_files or not filename:
|
||||||
|
# nothing got exported
|
||||||
|
raise ExportError(f"Could not export photo {uuid} ({lineno(__file__)})")
|
||||||
|
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
|
||||||
|
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
|
||||||
|
# TemporaryDirectory will cleanup on return
|
||||||
|
filename_stem = pathlib.Path(filename).stem
|
||||||
|
exported_paths = []
|
||||||
|
for fname in exported_files:
|
||||||
|
path = pathlib.Path(tmpdir.name) / fname
|
||||||
|
if (
|
||||||
|
len(exported_files) > 1
|
||||||
|
and not live_photo
|
||||||
|
and path.suffix.lower() == ".mov"
|
||||||
|
):
|
||||||
|
# it's the .mov part of live photo but not requested, so don't export
|
||||||
|
continue
|
||||||
|
if len(exported_files) > 1 and burst and path.stem != filename_stem:
|
||||||
|
# skip any burst photo that's not the one we asked for
|
||||||
|
continue
|
||||||
|
if filestem:
|
||||||
|
# rename the file based on filestem, keeping original extension
|
||||||
|
dest_new = dest / f"{filestem}{path.suffix}"
|
||||||
|
else:
|
||||||
|
# use the name Photos provided
|
||||||
|
dest_new = dest / path.name
|
||||||
|
if not dry_run:
|
||||||
|
if overwrite and dest_new.exists():
|
||||||
|
FileUtil.unlink(dest_new)
|
||||||
|
FileUtil.copy(str(path), str(dest_new))
|
||||||
|
exported_paths.append(str(dest_new))
|
||||||
|
return exported_paths
|
||||||
|
|
||||||
def _write_sidecar_files(
|
def _write_sidecar_files(
|
||||||
self,
|
self,
|
||||||
dest: pathlib.Path,
|
dest: pathlib.Path,
|
||||||
@@ -1236,14 +1381,18 @@ class PhotoExporter:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if write_sidecar:
|
if write_sidecar:
|
||||||
verbose(f"Writing {sidecar_type} sidecar {sidecar_filename}")
|
verbose(
|
||||||
|
f"Writing {sidecar_type} sidecar {self._filepath(sidecar_filename)}"
|
||||||
|
)
|
||||||
files_written.append(str(sidecar_filename))
|
files_written.append(str(sidecar_filename))
|
||||||
if not options.dry_run:
|
if not options.dry_run:
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||||
sidecar_record.digest = sidecar_digest
|
sidecar_record.digest = sidecar_digest
|
||||||
sidecar_record.dest_sig = fileutil.file_sig(sidecar_filename)
|
sidecar_record.dest_sig = fileutil.file_sig(sidecar_filename)
|
||||||
else:
|
else:
|
||||||
verbose(f"Skipped up to date {sidecar_type} sidecar {sidecar_filename}")
|
verbose(
|
||||||
|
f"Skipped up to date {sidecar_type} sidecar {self._filepath(sidecar_filename)}"
|
||||||
|
)
|
||||||
files_skipped.append(str(sidecar_filename))
|
files_skipped.append(str(sidecar_filename))
|
||||||
|
|
||||||
results = ExportResults(
|
results = ExportResults(
|
||||||
@@ -1306,7 +1455,9 @@ class PhotoExporter:
|
|||||||
# determine if we need to write the exif metadata
|
# determine if we need to write the exif metadata
|
||||||
# if we are not updating, we always write
|
# if we are not updating, we always write
|
||||||
# else, need to check the database to determine if we need to write
|
# else, need to check the database to determine if we need to write
|
||||||
verbose(f"Writing metadata with exiftool for {pathlib.Path(dest).name}")
|
verbose(
|
||||||
|
f"Writing metadata with exiftool for {self._filepath(pathlib.Path(dest).name)}"
|
||||||
|
)
|
||||||
if not options.dry_run:
|
if not options.dry_run:
|
||||||
warning_, error_ = self._write_exif_data(src, options=options)
|
warning_, error_ = self._write_exif_data(src, options=options)
|
||||||
if warning_:
|
if warning_:
|
||||||
@@ -1366,7 +1517,9 @@ class PhotoExporter:
|
|||||||
return exiftool.warning, exiftool.error
|
return exiftool.warning, exiftool.error
|
||||||
|
|
||||||
def _exiftool_dict(
|
def _exiftool_dict(
|
||||||
self, options: Optional[ExportOptions] = None, filename: Optional[str] = None
|
self,
|
||||||
|
options: t.Optional[ExportOptions] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
||||||
Does not include all the EXIF fields as those are likely already in the image.
|
Does not include all the EXIF fields as those are likely already in the image.
|
||||||
@@ -1668,9 +1821,9 @@ class PhotoExporter:
|
|||||||
|
|
||||||
def _exiftool_json_sidecar(
|
def _exiftool_json_sidecar(
|
||||||
self,
|
self,
|
||||||
options: Optional[ExportOptions] = None,
|
options: t.Optional[ExportOptions] = None,
|
||||||
tag_groups: bool = True,
|
tag_groups: bool = True,
|
||||||
filename: Optional[str] = None,
|
filename: t.Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
||||||
Does not include all the EXIF fields as those are likely already in the image.
|
Does not include all the EXIF fields as those are likely already in the image.
|
||||||
@@ -1721,13 +1874,15 @@ class PhotoExporter:
|
|||||||
return json.dumps([exif])
|
return json.dumps([exif])
|
||||||
|
|
||||||
def _xmp_sidecar(
|
def _xmp_sidecar(
|
||||||
self, options: Optional[ExportOptions] = None, extension: Optional[str] = None
|
self,
|
||||||
|
options: t.Optional[ExportOptions] = None,
|
||||||
|
extension: t.Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""returns string for XMP sidecar
|
"""returns string for XMP sidecar
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
options (ExportOptions): options for export
|
options (ExportOptions): options for export
|
||||||
extension (Optional[str]): which extension to use for SidecarForExtension property
|
extension (t.Optional[str]): which extension to use for SidecarForExtension property
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = options or ExportOptions()
|
options = options or ExportOptions()
|
||||||
@@ -1859,102 +2014,6 @@ def hexdigest(strval):
|
|||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def _export_photo_uuid_applescript(
|
|
||||||
uuid,
|
|
||||||
dest,
|
|
||||||
filestem=None,
|
|
||||||
original=True,
|
|
||||||
edited=False,
|
|
||||||
live_photo=False,
|
|
||||||
timeout=120,
|
|
||||||
burst=False,
|
|
||||||
dry_run=False,
|
|
||||||
overwrite=False,
|
|
||||||
):
|
|
||||||
"""Export photo to dest path using applescript to control Photos
|
|
||||||
If photo is a live photo, exports both the photo and associated .mov file
|
|
||||||
|
|
||||||
Args:
|
|
||||||
uuid: UUID of photo to export
|
|
||||||
dest: destination path to export to
|
|
||||||
filestem: (string) if provided, exported filename will be named stem.ext
|
|
||||||
where ext is extension of the file exported by photos (e.g. .jpeg, .mov, etc)
|
|
||||||
If not provided, file will be named with whatever name Photos uses
|
|
||||||
If filestem.ext exists, it wil be overwritten
|
|
||||||
original: (boolean) if True, export original image; default = True
|
|
||||||
edited: (boolean) if True, export edited photo; default = False
|
|
||||||
If photo not edited and edited=True, will still export the original image
|
|
||||||
caller must verify image has been edited
|
|
||||||
*Note*: must be called with either edited or original but not both,
|
|
||||||
will raise error if called with both edited and original = True
|
|
||||||
live_photo: (boolean) if True, export associated .mov live photo; default = False
|
|
||||||
timeout: timeout value in seconds; export will fail if applescript run time exceeds timeout
|
|
||||||
burst: (boolean) set to True if file is a burst image to avoid Photos export error
|
|
||||||
dry_run: (boolean) set to True to run in "dry run" mode which will download file but not actually copy to destination
|
|
||||||
|
|
||||||
Returns: list of paths to exported file(s) or None if export failed
|
|
||||||
|
|
||||||
Raises: ExportError if error during export
|
|
||||||
|
|
||||||
Note: For Live Photos, if edited=True, will export a jpeg but not the movie, even if photo
|
|
||||||
has not been edited. This is due to how Photos Applescript interface works.
|
|
||||||
"""
|
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
|
||||||
if not dest.is_dir():
|
|
||||||
raise ValueError(f"dest {dest} must be a directory")
|
|
||||||
|
|
||||||
if not original ^ edited:
|
|
||||||
raise ValueError("edited or original must be True but not both")
|
|
||||||
|
|
||||||
tmpdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
|
||||||
|
|
||||||
exported_files = []
|
|
||||||
filename = None
|
|
||||||
try:
|
|
||||||
# I've seen intermittent failures with the PhotoScript export so retry if
|
|
||||||
# export doesn't return anything
|
|
||||||
retries = 0
|
|
||||||
while not exported_files and retries < MAX_PHOTOSCRIPT_RETRIES:
|
|
||||||
photo = photoscript.Photo(uuid)
|
|
||||||
filename = photo.filename
|
|
||||||
exported_files = photo.export(
|
|
||||||
tmpdir.name, original=original, timeout=timeout
|
|
||||||
)
|
|
||||||
retries += 1
|
|
||||||
except Exception as e:
|
|
||||||
raise ExportError(e)
|
|
||||||
|
|
||||||
if not exported_files or not filename:
|
|
||||||
# nothing got exported
|
|
||||||
raise ExportError(f"Could not export photo {uuid} ({lineno(__file__)})")
|
|
||||||
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
|
|
||||||
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
|
|
||||||
# TemporaryDirectory will cleanup on return
|
|
||||||
filename_stem = pathlib.Path(filename).stem
|
|
||||||
exported_paths = []
|
|
||||||
for fname in exported_files:
|
|
||||||
path = pathlib.Path(tmpdir.name) / fname
|
|
||||||
if len(exported_files) > 1 and not live_photo and path.suffix.lower() == ".mov":
|
|
||||||
# it's the .mov part of live photo but not requested, so don't export
|
|
||||||
continue
|
|
||||||
if len(exported_files) > 1 and burst and path.stem != filename_stem:
|
|
||||||
# skip any burst photo that's not the one we asked for
|
|
||||||
continue
|
|
||||||
if filestem:
|
|
||||||
# rename the file based on filestem, keeping original extension
|
|
||||||
dest_new = dest / f"{filestem}{path.suffix}"
|
|
||||||
else:
|
|
||||||
# use the name Photos provided
|
|
||||||
dest_new = dest / path.name
|
|
||||||
if not dry_run:
|
|
||||||
if overwrite and dest_new.exists():
|
|
||||||
FileUtil.unlink(dest_new)
|
|
||||||
FileUtil.copy(str(path), str(dest_new))
|
|
||||||
exported_paths.append(str(dest_new))
|
|
||||||
return exported_paths
|
|
||||||
|
|
||||||
|
|
||||||
def _check_export_suffix(src, dest, edited):
|
def _check_export_suffix(src, dest, edited):
|
||||||
"""Helper function for exporting photos to check file extensions of destination path.
|
"""Helper function for exporting photos to check file extensions of destination path.
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ from .scoreinfo import ScoreInfo
|
|||||||
from .searchinfo import SearchInfo
|
from .searchinfo import SearchInfo
|
||||||
from .text_detection import detect_text
|
from .text_detection import detect_text
|
||||||
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
||||||
from .utils import _debug, _get_resource_loc, list_directory, _debug
|
from .utils import _get_resource_loc, list_directory
|
||||||
|
|
||||||
__all__ = ["PhotoInfo", "PhotoInfoNone"]
|
__all__ = ["PhotoInfo", "PhotoInfoNone"]
|
||||||
|
|
||||||
|
|||||||
@@ -50,15 +50,16 @@ from .._constants import (
|
|||||||
from .._version import __version__
|
from .._version import __version__
|
||||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
|
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
|
||||||
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||||
|
from ..debug import is_debug
|
||||||
from ..fileutil import FileUtil
|
from ..fileutil import FileUtil
|
||||||
from ..personinfo import PersonInfo
|
from ..personinfo import PersonInfo
|
||||||
from ..photoinfo import PhotoInfo
|
from ..photoinfo import PhotoInfo
|
||||||
from ..phototemplate import RenderOptions
|
from ..phototemplate import RenderOptions
|
||||||
from ..queryoptions import QueryOptions
|
from ..queryoptions import QueryOptions
|
||||||
|
from ..rich_utils import add_rich_markup_tag
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
_check_file_exists,
|
_check_file_exists,
|
||||||
_db_is_locked,
|
_db_is_locked,
|
||||||
_debug,
|
|
||||||
_get_os_version,
|
_get_os_version,
|
||||||
_open_sql_file,
|
_open_sql_file,
|
||||||
get_last_library_path,
|
get_last_library_path,
|
||||||
@@ -90,13 +91,14 @@ class PhotosDB:
|
|||||||
labels_normalized_as_dict,
|
labels_normalized_as_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, dbfile=None, verbose=None, exiftool=None):
|
def __init__(self, dbfile=None, verbose=None, exiftool=None, rich=None):
|
||||||
"""Create a new PhotosDB object.
|
"""Create a new PhotosDB object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
|
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
|
||||||
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||||
exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
|
exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
|
||||||
|
rich: use rich with verbose output
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError if dbfile is not a valid Photos library.
|
FileNotFoundError if dbfile is not a valid Photos library.
|
||||||
@@ -119,6 +121,12 @@ class PhotosDB:
|
|||||||
raise TypeError("verbose must be callable")
|
raise TypeError("verbose must be callable")
|
||||||
self._verbose = verbose
|
self._verbose = verbose
|
||||||
|
|
||||||
|
# define functions for adding markup
|
||||||
|
self._filepath = add_rich_markup_tag("filepath", rich=rich)
|
||||||
|
self._filename = add_rich_markup_tag("filename", rich=rich)
|
||||||
|
self._uuid = add_rich_markup_tag("uuid", rich=rich)
|
||||||
|
self._num = add_rich_markup_tag("num", rich=rich)
|
||||||
|
|
||||||
# enable beta features
|
# enable beta features
|
||||||
self._beta = False
|
self._beta = False
|
||||||
|
|
||||||
@@ -264,7 +272,7 @@ class PhotosDB:
|
|||||||
# key is Z_PK of ZMOMENT table and values are the moment info
|
# key is Z_PK of ZMOMENT table and values are the moment info
|
||||||
self._db_moment_pk = {}
|
self._db_moment_pk = {}
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"dbfile = {dbfile}")
|
logging.debug(f"dbfile = {dbfile}")
|
||||||
|
|
||||||
if dbfile is None:
|
if dbfile is None:
|
||||||
@@ -281,7 +289,7 @@ class PhotosDB:
|
|||||||
if not _check_file_exists(dbfile):
|
if not _check_file_exists(dbfile):
|
||||||
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"dbfile = {dbfile}")
|
logging.debug(f"dbfile = {dbfile}")
|
||||||
|
|
||||||
# init database names
|
# init database names
|
||||||
@@ -295,7 +303,7 @@ class PhotosDB:
|
|||||||
# or photosanalysisd
|
# or photosanalysisd
|
||||||
self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile)
|
self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile)
|
||||||
|
|
||||||
verbose(f"Processing database {self._dbfile}")
|
verbose(f"Processing database {self._filepath(self._dbfile)}")
|
||||||
|
|
||||||
# if database is exclusively locked, make a copy of it and use the copy
|
# if database is exclusively locked, make a copy of it and use the copy
|
||||||
# Photos maintains an exclusive lock on the database file while Photos is open
|
# Photos maintains an exclusive lock on the database file while Photos is open
|
||||||
@@ -315,13 +323,13 @@ class PhotosDB:
|
|||||||
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
||||||
else:
|
else:
|
||||||
self._dbfile_actual = self._tmp_db = dbfile
|
self._dbfile_actual = self._tmp_db = dbfile
|
||||||
verbose(f"Processing database {self._dbfile_actual}")
|
verbose(f"Processing database {self._filepath(self._dbfile_actual)}")
|
||||||
# if database is exclusively locked, make a copy of it and use the copy
|
# if database is exclusively locked, make a copy of it and use the copy
|
||||||
if _db_is_locked(self._dbfile_actual):
|
if _db_is_locked(self._dbfile_actual):
|
||||||
verbose(f"Database locked, creating temporary copy.")
|
verbose(f"Database locked, creating temporary copy.")
|
||||||
self._tmp_db = self._copy_db_file(self._dbfile_actual)
|
self._tmp_db = self._copy_db_file(self._dbfile_actual)
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
||||||
)
|
)
|
||||||
@@ -336,7 +344,7 @@ class PhotosDB:
|
|||||||
masters_path = os.path.join(library_path, "originals")
|
masters_path = os.path.join(library_path, "originals")
|
||||||
self._masters_path = masters_path
|
self._masters_path = masters_path
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"library = {library_path}, masters = {masters_path}")
|
logging.debug(f"library = {library_path}, masters = {masters_path}")
|
||||||
|
|
||||||
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
|
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
|
||||||
@@ -592,7 +600,7 @@ class PhotosDB:
|
|||||||
print(f"Error copying{fname} to {dest_path}", file=sys.stderr)
|
print(f"Error copying{fname} to {dest_path}", file=sys.stderr)
|
||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(dest_path)
|
logging.debug(dest_path)
|
||||||
|
|
||||||
return dest_path
|
return dest_path
|
||||||
@@ -619,7 +627,7 @@ class PhotosDB:
|
|||||||
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
|
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
|
||||||
# raise Exception
|
# raise Exception
|
||||||
|
|
||||||
# if _debug():
|
# if is_debug():
|
||||||
# logging.debug(dest_path)
|
# logging.debug(dest_path)
|
||||||
|
|
||||||
# return dest_path
|
# return dest_path
|
||||||
@@ -630,7 +638,7 @@ class PhotosDB:
|
|||||||
|
|
||||||
verbose = self._verbose
|
verbose = self._verbose
|
||||||
verbose("Processing database.")
|
verbose("Processing database.")
|
||||||
verbose(f"Database version: {self._db_version}.")
|
verbose(f"Database version: {self._num(self._db_version)}.")
|
||||||
|
|
||||||
self._photos_ver = 4 # only used in Photos 5+
|
self._photos_ver = 4 # only used in Photos 5+
|
||||||
|
|
||||||
@@ -1079,7 +1087,7 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
|
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
|
||||||
else:
|
else:
|
||||||
# unknown
|
# unknown
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"WARNING: {uuid} found unknown type {row[21]}")
|
logging.debug(f"WARNING: {uuid} found unknown type {row[21]}")
|
||||||
self._dbphotos[uuid]["type"] = None
|
self._dbphotos[uuid]["type"] = None
|
||||||
|
|
||||||
@@ -1302,7 +1310,7 @@ class PhotosDB:
|
|||||||
and row[6] == 2
|
and row[6] == 2
|
||||||
):
|
):
|
||||||
if "edit_resource_id" in self._dbphotos[uuid]:
|
if "edit_resource_id" in self._dbphotos[uuid]:
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"WARNING: found more than one edit_resource_id for "
|
f"WARNING: found more than one edit_resource_id for "
|
||||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||||
@@ -1581,7 +1589,7 @@ class PhotosDB:
|
|||||||
but it works so don't touch it.
|
but it works so don't touch it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"_process_database5")
|
logging.debug(f"_process_database5")
|
||||||
verbose = self._verbose
|
verbose = self._verbose
|
||||||
verbose(f"Processing database.")
|
verbose(f"Processing database.")
|
||||||
@@ -1590,7 +1598,9 @@ class PhotosDB:
|
|||||||
# some of the tables/columns have different names in different versions of Photos
|
# some of the tables/columns have different names in different versions of Photos
|
||||||
photos_ver = get_db_model_version(self._tmp_db)
|
photos_ver = get_db_model_version(self._tmp_db)
|
||||||
self._photos_ver = photos_ver
|
self._photos_ver = photos_ver
|
||||||
verbose(f"Database version: {self._db_version}, {photos_ver}.")
|
verbose(
|
||||||
|
f"Database version: {self._num(self._db_version)}, {self._num(photos_ver)}."
|
||||||
|
)
|
||||||
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||||
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
||||||
asset_album_table = _DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_TABLE"]
|
asset_album_table = _DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_TABLE"]
|
||||||
@@ -1603,7 +1613,7 @@ class PhotosDB:
|
|||||||
hdr_type_column = _DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]
|
hdr_type_column = _DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]
|
||||||
|
|
||||||
# Look for all combinations of persons and pictures
|
# Look for all combinations of persons and pictures
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"Getting information about persons")
|
logging.debug(f"Getting information about persons")
|
||||||
|
|
||||||
# get info to associate persons with photos
|
# get info to associate persons with photos
|
||||||
@@ -2012,7 +2022,7 @@ class PhotosDB:
|
|||||||
elif row[17] == 1:
|
elif row[17] == 1:
|
||||||
info["type"] = _MOVIE_TYPE
|
info["type"] = _MOVIE_TYPE
|
||||||
else:
|
else:
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(f"WARNING: {uuid} found unknown type {row[17]}")
|
logging.debug(f"WARNING: {uuid} found unknown type {row[17]}")
|
||||||
info["type"] = None
|
info["type"] = None
|
||||||
|
|
||||||
@@ -2211,7 +2221,7 @@ class PhotosDB:
|
|||||||
if uuid in self._dbphotos:
|
if uuid in self._dbphotos:
|
||||||
self._dbphotos[uuid]["extendedDescription"] = normalize_unicode(row[1])
|
self._dbphotos[uuid]["extendedDescription"] = normalize_unicode(row[1])
|
||||||
else:
|
else:
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||||
)
|
)
|
||||||
@@ -2230,7 +2240,7 @@ class PhotosDB:
|
|||||||
if uuid in self._dbphotos:
|
if uuid in self._dbphotos:
|
||||||
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
|
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
|
||||||
else:
|
else:
|
||||||
if _debug():
|
if is_debug():
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Template statements may contain one or more modifiers. The full syntax is:
|
|||||||
|
|
||||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||||
|
|
||||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title" the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||||
|
|
||||||
|
|
||||||
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||||
|
|||||||
24
osxphotos/rich_utils.py
Normal file
24
osxphotos/rich_utils.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""utilities for working with rich markup"""
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
def add_rich_markup_tag(tag: str, rich=True) -> Callable:
|
||||||
|
"""Returns function that rich markup tags to string"""
|
||||||
|
|
||||||
|
if not rich:
|
||||||
|
return no_markup
|
||||||
|
|
||||||
|
def add_tag(msg: str) -> str:
|
||||||
|
"""Add tag to string"""
|
||||||
|
return f"[{tag}]{msg}[/{tag}]"
|
||||||
|
|
||||||
|
return add_tag
|
||||||
|
|
||||||
|
|
||||||
|
def no_markup(msg: str) -> str:
|
||||||
|
"""Return msg without markup"""
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["add_rich_markup_tag", "no_markup"]
|
||||||
@@ -355,7 +355,6 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
|
|||||||
|
|
||||||
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
||||||
|
|
||||||
|
|
||||||
### An example from an actual osxphotos user
|
### An example from an actual osxphotos user
|
||||||
|
|
||||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||||
@@ -390,6 +389,10 @@ Here's a comprehensive use case from an actual osxphotos user that integrates ma
|
|||||||
|
|
||||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||||
|
|
||||||
|
### Color Themes
|
||||||
|
|
||||||
|
Some osxphotos commands such as export use color themes to colorize the output to make it more legible. The theme may be specified with the `--theme` option. For example: `osxphotos export /path/to/export --verbose --theme dark` uses a theme suited for dark terminals. If you don't specify the color theme, osxphotos will select a default theme based on the current terminal settings. You can also specify your own default theme. See `osxphotos help theme` for more information on themes and for commands to help manage themes. Themes are defined in `.theme` files in the `~/.osxphotos/themes` directory and use style specifications compatible with the [rich](https://rich.readthedocs.io/en/stable/style.html) library.
|
||||||
|
|
||||||
### Conclusion
|
### Conclusion
|
||||||
|
|
||||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||||
@@ -20,8 +20,6 @@ from plistlib import load as plistload
|
|||||||
from typing import Callable, List, Union, Optional
|
from typing import Callable, List, Union, Optional
|
||||||
|
|
||||||
import CoreFoundation
|
import CoreFoundation
|
||||||
import objc
|
|
||||||
from Foundation import NSFileManager, NSPredicate, NSString
|
|
||||||
|
|
||||||
from ._constants import UNICODE_FORMAT
|
from ._constants import UNICODE_FORMAT
|
||||||
|
|
||||||
@@ -41,17 +39,12 @@ __all__ = [
|
|||||||
"normalize_unicode",
|
"normalize_unicode",
|
||||||
]
|
]
|
||||||
|
|
||||||
_DEBUG = False
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not _DEBUG:
|
|
||||||
logging.disable(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_logger():
|
def _get_logger():
|
||||||
"""Used only for testing
|
"""Used only for testing
|
||||||
@@ -62,21 +55,6 @@ def _get_logger():
|
|||||||
return logging.Logger(__name__)
|
return logging.Logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _set_debug(debug):
|
|
||||||
"""Enable or disable debug logging"""
|
|
||||||
global _DEBUG
|
|
||||||
_DEBUG = debug
|
|
||||||
if debug:
|
|
||||||
logging.disable(logging.NOTSET)
|
|
||||||
else:
|
|
||||||
logging.disable(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def _debug():
|
|
||||||
"""returns True if debugging turned on (via _set_debug), otherwise, false"""
|
|
||||||
return _DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
"""do nothing (no operation)"""
|
"""do nothing (no operation)"""
|
||||||
pass
|
pass
|
||||||
@@ -270,8 +248,7 @@ def list_photo_libraries():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# On older OS, may not get all libraries so make sure we get the last one
|
# On older OS, may not get all libraries so make sure we get the last one
|
||||||
last_lib = get_last_library_path()
|
if last_lib := get_last_library_path():
|
||||||
if last_lib:
|
|
||||||
lib_list.append(last_lib)
|
lib_list.append(last_lib)
|
||||||
|
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
@@ -279,8 +256,7 @@ def list_photo_libraries():
|
|||||||
).splitlines()
|
).splitlines()
|
||||||
for lib in output:
|
for lib in output:
|
||||||
lib_list.append(lib.decode("utf-8"))
|
lib_list.append(lib.decode("utf-8"))
|
||||||
lib_list = list(set(lib_list))
|
lib_list = sorted(set(lib_list))
|
||||||
lib_list.sort()
|
|
||||||
return lib_list
|
return lib_list
|
||||||
|
|
||||||
|
|
||||||
@@ -505,8 +481,11 @@ def load_function(pyfile: str, function_name: str) -> Callable:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
func = getattr(module, function_name)
|
func = getattr(module, function_name)
|
||||||
except AttributeError:
|
except AttributeError as e:
|
||||||
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
|
raise ValueError(
|
||||||
|
f"'{function_name}' not found in module '{module_name}'"
|
||||||
|
) from e
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# restore sys.path
|
# restore sys.path
|
||||||
sys.path = syspath
|
sys.path = syspath
|
||||||
|
|||||||
2
pytest.ini
Normal file
2
pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = -p tests.plugins.env_vars
|
||||||
@@ -20,6 +20,8 @@ pyobjc-framework-Quartz>=7.3,<9.0
|
|||||||
pyobjc-framework-Vision>=7.3,<9.0
|
pyobjc-framework-Vision>=7.3,<9.0
|
||||||
PyYAML>=5.4.1,<6.0.0
|
PyYAML>=5.4.1,<6.0.0
|
||||||
rich>=11.2.0,<12.0.0
|
rich>=11.2.0,<12.0.0
|
||||||
|
rich_theme_manager>=0.7.0
|
||||||
textx>=2.3.0,<2.4.0
|
textx>=2.3.0,<2.4.0
|
||||||
toml>=0.10.2,<0.11.0
|
toml>=0.10.2,<0.11.0
|
||||||
|
wrapt>=1.13.3,<1.14.0
|
||||||
wurlitzer>=2.1.0,<2.2.0
|
wurlitzer>=2.1.0,<2.2.0
|
||||||
2
setup.py
2
setup.py
@@ -95,8 +95,10 @@ setup(
|
|||||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||||
"rich>=11.2.0,<12.0.0",
|
"rich>=11.2.0,<12.0.0",
|
||||||
|
"rich_theme_manager>=0.7.0",
|
||||||
"textx>=2.3.0,<3.0.0",
|
"textx>=2.3.0,<3.0.0",
|
||||||
"toml>=0.10.2,<0.11.0",
|
"toml>=0.10.2,<0.11.0",
|
||||||
|
"wrapt>=1.13.3,<1.14.0",
|
||||||
"wurlitzer>=2.1.0,<3.0.0",
|
"wurlitzer>=2.1.0,<3.0.0",
|
||||||
],
|
],
|
||||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},
|
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -8,6 +8,8 @@ from photoscript.utils import ditto
|
|||||||
|
|
||||||
from osxphotos.exiftool import _ExifToolProc
|
from osxphotos.exiftool import _ExifToolProc
|
||||||
|
|
||||||
|
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
|
||||||
|
|
||||||
|
|
||||||
def get_os_version():
|
def get_os_version():
|
||||||
import platform
|
import platform
|
||||||
|
|||||||
0
tests/plugins/__init__.py
Normal file
0
tests/plugins/__init__.py
Normal file
8
tests/plugins/env_vars.py
Normal file
8
tests/plugins/env_vars.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_load_initial_conftests(args, early_config, parser):
|
||||||
|
os.environ["OSXPHOTOS_IS_TESTING"] = "1"
|
||||||
@@ -17,7 +17,6 @@ from tempfile import TemporaryDirectory
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from conftest import copy_photos_library_to_path
|
|
||||||
from osxmetadata import OSXMetaData, Tag
|
from osxmetadata import OSXMetaData, Tag
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -37,6 +36,8 @@ from osxphotos.exiftool import ExifTool, get_exiftool_path
|
|||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
from osxphotos.utils import noop, normalize_fs_path, normalize_unicode
|
from osxphotos.utils import noop, normalize_fs_path, normalize_unicode
|
||||||
|
|
||||||
|
from .conftest import copy_photos_library_to_path
|
||||||
|
|
||||||
CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary"
|
CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary"
|
||||||
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
|
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
|
||||||
RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary"
|
RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary"
|
||||||
@@ -58,6 +59,11 @@ UUID_BURST_ALBUM = {
|
|||||||
"TestBurst/IMG_9815.JPG",
|
"TestBurst/IMG_9815.JPG",
|
||||||
"TestBurst/IMG_9816.JPG",
|
"TestBurst/IMG_9816.JPG",
|
||||||
"TestBurst2/IMG_9814.JPG",
|
"TestBurst2/IMG_9814.JPG",
|
||||||
|
"osxphotos/IMG_9812.JPG", # in my personal library, IMG_9812.JPG == "9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F"
|
||||||
|
"osxphotos/IMG_9813.JPG",
|
||||||
|
"osxphotos/IMG_9814.JPG",
|
||||||
|
"osxphotos/IMG_9815.JPG",
|
||||||
|
"osxphotos/IMG_9816.JPG",
|
||||||
],
|
],
|
||||||
"75154738-83AA-4DCD-A913-632D5D1C0FEE": [
|
"75154738-83AA-4DCD-A913-632D5D1C0FEE": [
|
||||||
"TestBurst/IMG_9812.JPG",
|
"TestBurst/IMG_9812.JPG",
|
||||||
@@ -66,6 +72,11 @@ UUID_BURST_ALBUM = {
|
|||||||
"TestBurst/IMG_9815.JPG",
|
"TestBurst/IMG_9815.JPG",
|
||||||
"TestBurst/IMG_9816.JPG",
|
"TestBurst/IMG_9816.JPG",
|
||||||
"TestBurst2/IMG_9814.JPG",
|
"TestBurst2/IMG_9814.JPG",
|
||||||
|
"osxphotos/IMG_9812.JPG", # in my personal library, IMG_9812.JPG == "9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F"
|
||||||
|
"osxphotos/IMG_9813.JPG",
|
||||||
|
"osxphotos/IMG_9814.JPG",
|
||||||
|
"osxphotos/IMG_9815.JPG",
|
||||||
|
"osxphotos/IMG_9816.JPG",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1384,7 +1395,7 @@ def test_query_exif_case_insensitive(exiftag, exifvalue, uuid_expected):
|
|||||||
|
|
||||||
|
|
||||||
def test_export():
|
def test_export():
|
||||||
|
"""test basic export"""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
@@ -1395,6 +1406,22 @@ def test_export():
|
|||||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_tmpdir():
|
||||||
|
"""test basic export with --tmpdir"""
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
tmpdir = TemporaryDirectory()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
export,
|
||||||
|
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--tmpdir", tmpdir.name],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
def test_export_uuid_from_file():
|
def test_export_uuid_from_file():
|
||||||
"""Test export with --uuid-from-file"""
|
"""Test export with --uuid-from-file"""
|
||||||
|
|
||||||
@@ -1811,6 +1838,40 @@ def test_export_exiftool():
|
|||||||
assert exif[key] == CLI_EXIFTOOL[uuid][key]
|
assert exif[key] == CLI_EXIFTOOL[uuid][key]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
|
def test_export_exiftool_tmpdir():
|
||||||
|
"""test --exiftool with --tmpdir"""
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
tmpdir = TemporaryDirectory()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
for uuid in CLI_EXIFTOOL:
|
||||||
|
result = runner.invoke(
|
||||||
|
export,
|
||||||
|
[
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"-V",
|
||||||
|
"--exiftool",
|
||||||
|
"--uuid",
|
||||||
|
f"{uuid}",
|
||||||
|
"--tmpdir",
|
||||||
|
tmpdir.name,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]])
|
||||||
|
|
||||||
|
exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).asdict()
|
||||||
|
for key in CLI_EXIFTOOL[uuid]:
|
||||||
|
if type(exif[key]) == list:
|
||||||
|
assert sorted(exif[key]) == sorted(CLI_EXIFTOOL[uuid][key])
|
||||||
|
else:
|
||||||
|
assert exif[key] == CLI_EXIFTOOL[uuid][key]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
def test_export_exiftool_template_change():
|
def test_export_exiftool_template_change():
|
||||||
"""Test --exiftool when template changes with --update, #630"""
|
"""Test --exiftool when template changes with --update, #630"""
|
||||||
@@ -6406,8 +6467,7 @@ def test_export_burst_uuid():
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
# subtract 1 from len because one photo in two albums so shows up twice in the list
|
assert f"exported: 5" in result.output
|
||||||
assert f"exported: {len(UUID_BURST_ALBUM[uuid]) - 1}" in result.output
|
|
||||||
|
|
||||||
# export again with --skip-bursts
|
# export again with --skip-bursts
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -6503,7 +6563,7 @@ def test_export_download_missing_preview():
|
|||||||
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
||||||
reason="Skip if not running on author's personal library.",
|
reason="Skip if not running on author's personal library.",
|
||||||
)
|
)
|
||||||
def test_export_download_missing_preview_applesccript():
|
def test_export_download_missing_preview_applescript():
|
||||||
"""test --download-missing --preview and applescript download, #564"""
|
"""test --download-missing --preview and applescript download, #564"""
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|||||||
20
tests/test_debug.py
Normal file
20
tests/test_debug.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Test debug"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.debug import is_debug, set_debug
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_enable():
|
||||||
|
set_debug(True)
|
||||||
|
logger = osxphotos._get_logger()
|
||||||
|
assert logger.isEnabledFor(logging.DEBUG)
|
||||||
|
assert is_debug()
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_disable():
|
||||||
|
set_debug(False)
|
||||||
|
logger = osxphotos._get_logger()
|
||||||
|
assert not logger.isEnabledFor(logging.DEBUG)
|
||||||
|
assert not is_debug()
|
||||||
@@ -12,40 +12,40 @@ pytestmark = pytest.mark.skipif(
|
|||||||
)
|
)
|
||||||
|
|
||||||
UUID_DICT = {
|
UUID_DICT = {
|
||||||
"has_adjustments": "C925CFDC-FF2B-4E71-AC9D-C669B6453A8B", # IMG_1929.JPG
|
"has_adjustments": "C925CFDC-FF2B-4E71-AC9D-C669B6453A8B", # IMG_1929.JPG
|
||||||
"no_adjustments": "16A6AF6B-D8FC-4256-AE33-889733E3EEAB", # IMG_9847.JPG
|
"no_adjustments": "16A6AF6B-D8FC-4256-AE33-889733E3EEAB", # IMG_9847.JPG
|
||||||
"live": "8EC216A2-0032-4934-BD3F-04C6259B3304", # IMG_3259.HEIC
|
"live": "8EC216A2-0032-4934-BD3F-04C6259B3304", # IMG_3259.HEIC
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID_BURSTS = {
|
UUID_BURSTS = {
|
||||||
"9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F": {
|
"9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F": {
|
||||||
"selected": False,
|
"selected": False,
|
||||||
"filename": "IMG_9812.JPG",
|
"filename": "IMG_9812.JPG",
|
||||||
"burst_albums": ["TestBurst"],
|
"burst_albums": ["TestBurst", "osxphotos"],
|
||||||
"albums": ["TestBurst"],
|
"albums": ["TestBurst", "osxphotos"],
|
||||||
},
|
},
|
||||||
"89E235DD-B9AC-4E8D-BDA2-986981CA7582": {
|
"89E235DD-B9AC-4E8D-BDA2-986981CA7582": {
|
||||||
"selected": False,
|
"selected": False,
|
||||||
"filename": "IMG_9813.JPG",
|
"filename": "IMG_9813.JPG",
|
||||||
"burst_albums": ["TestBurst"],
|
"burst_albums": ["TestBurst", "osxphotos"],
|
||||||
"albums": [],
|
"albums": [],
|
||||||
},
|
},
|
||||||
"75154738-83AA-4DCD-A913-632D5D1C0FEE": {
|
"75154738-83AA-4DCD-A913-632D5D1C0FEE": {
|
||||||
"selected": True,
|
"selected": True,
|
||||||
"filename": "IMG_9814.JPG",
|
"filename": "IMG_9814.JPG",
|
||||||
"burst_albums": ["TestBurst", "TestBurst2"],
|
"burst_albums": ["TestBurst", "TestBurst2", "osxphotos"],
|
||||||
"albums": ["TestBurst2"],
|
"albums": ["TestBurst2"],
|
||||||
},
|
},
|
||||||
"4A836160-51B2-4E32-907D-ECDDB2CEC657": {
|
"4A836160-51B2-4E32-907D-ECDDB2CEC657": {
|
||||||
"selected": False,
|
"selected": False,
|
||||||
"filename": "IMG_9815.JPG",
|
"filename": "IMG_9815.JPG",
|
||||||
"burst_albums": ["TestBurst"],
|
"burst_albums": ["TestBurst", "osxphotos"],
|
||||||
"albums": [],
|
"albums": [],
|
||||||
},
|
},
|
||||||
"F5E6BD24-B493-44E9-BDA2-7AD9D2CC8C9D": {
|
"F5E6BD24-B493-44E9-BDA2-7AD9D2CC8C9D": {
|
||||||
"selected": True,
|
"selected": True,
|
||||||
"filename": "IMG_9816.JPG",
|
"filename": "IMG_9816.JPG",
|
||||||
"burst_albums": ["TestBurst"],
|
"burst_albums": ["TestBurst", "osxphotos"],
|
||||||
"albums": [],
|
"albums": [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
""" test FileUtil """
|
""" test FileUtil """
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
TEST_HEIC = "tests/test-images/IMG_3092.heic"
|
TEST_HEIC = "tests/test-images/IMG_3092.heic"
|
||||||
TEST_RAW = "tests/test-images/DSC03584.dng"
|
TEST_RAW = "tests/test-images/DSC03584.dng"
|
||||||
|
|
||||||
@@ -11,6 +15,7 @@ def test_copy_file_valid():
|
|||||||
# copy file with valid src, dest
|
# copy file with valid src, dest
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -23,6 +28,7 @@ def test_copy_file_valid():
|
|||||||
def test_copy_file_invalid():
|
def test_copy_file_invalid():
|
||||||
# copy file with invalid src
|
# copy file with invalid src
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -36,6 +42,7 @@ def test_hardlink_file_valid():
|
|||||||
# hardlink file with valid src, dest
|
# hardlink file with valid src, dest
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -49,6 +56,7 @@ def test_hardlink_file_valid():
|
|||||||
def test_unlink_file():
|
def test_unlink_file():
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -63,6 +71,7 @@ def test_unlink_file():
|
|||||||
def test_rmdir():
|
def test_rmdir():
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -77,9 +86,10 @@ def test_rmdir():
|
|||||||
reason="Skip if running in Github actions, no GPU.",
|
reason="Skip if running in Github actions, no GPU.",
|
||||||
)
|
)
|
||||||
def test_convert_to_jpeg():
|
def test_convert_to_jpeg():
|
||||||
""" test convert_to_jpeg """
|
"""test convert_to_jpeg"""
|
||||||
import pathlib
|
import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -95,9 +105,10 @@ def test_convert_to_jpeg():
|
|||||||
reason="Skip if running in Github actions, no GPU.",
|
reason="Skip if running in Github actions, no GPU.",
|
||||||
)
|
)
|
||||||
def test_convert_to_jpeg_quality():
|
def test_convert_to_jpeg_quality():
|
||||||
""" test convert_to_jpeg with compression_quality """
|
"""test convert_to_jpeg with compression_quality"""
|
||||||
import pathlib
|
import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -113,6 +124,7 @@ def test_rename_file():
|
|||||||
# rename file with valid src, dest
|
# rename file with valid src, dest
|
||||||
import pathlib
|
import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -125,3 +137,15 @@ def test_rename_file():
|
|||||||
assert pathlib.Path(dest2).exists()
|
assert pathlib.Path(dest2).exists()
|
||||||
assert not pathlib.Path(dest).exists()
|
assert not pathlib.Path(dest).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_tempdir():
|
||||||
|
"""Test FileUtil.tmpdir"""
|
||||||
|
tmpdir = FileUtil.tmpdir()
|
||||||
|
assert pathlib.Path(tmpdir.name).is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_tempdir_context_mgr():
|
||||||
|
"""Test Fileutil.tmpdir as context manager"""
|
||||||
|
with FileUtil.tmpdir() as tmpdir_name:
|
||||||
|
assert pathlib.Path(tmpdir_name).is_dir()
|
||||||
|
assert not pathlib.Path(tmpdir_name).is_dir()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from photoinfo_mock import PhotoInfoMock
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
@@ -15,6 +14,8 @@ from osxphotos.phototemplate import (
|
|||||||
RenderOptions,
|
RenderOptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .photoinfo_mock import PhotoInfoMock
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exiftool = get_exiftool_path()
|
exiftool = get_exiftool_path()
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -21,18 +21,6 @@ from osxphotos.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_debug_enable():
|
|
||||||
osxphotos._set_debug(True)
|
|
||||||
logger = osxphotos._get_logger()
|
|
||||||
assert logger.isEnabledFor(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def test_debug_disable():
|
|
||||||
osxphotos._set_debug(False)
|
|
||||||
logger = osxphotos._get_logger()
|
|
||||||
assert not logger.isEnabledFor(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dd_to_dms():
|
def test_dd_to_dms():
|
||||||
# expands coverage for edge case in _dd_to_dms
|
# expands coverage for edge case in _dd_to_dms
|
||||||
|
|
||||||
|
|||||||
@@ -73,11 +73,11 @@ def generate_help_text(command):
|
|||||||
|
|
||||||
# get current help text
|
# get current help text
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
result = runner.invoke(cli_main, ["help", command])
|
result = runner.invoke(cli_main, ["help", command, "--width", 78])
|
||||||
help_txt = result.output
|
help_txt = result.output
|
||||||
|
|
||||||
# running the help command above doesn't output the full "Usage" line
|
# running the help command above doesn't output the full "Usage" line
|
||||||
help_txt = help_txt.replace(f"Usage: cli-main", f"Usage: osxphotos")
|
help_txt = help_txt.replace("Usage: cli-main", "Usage: osxphotos")
|
||||||
return help_txt
|
return help_txt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user