Compare commits

...

37 Commits

Author SHA1 Message Date
Rhet Turnbull
feb9538d1c Fix for --load-config, #643 2022-02-27 09:50:25 -08:00
Rhet Turnbull
b275280a1f Updated README.md 2022-02-26 23:25:16 -08:00
Rhet Turnbull
924ef72446 Updated CHANGELOG.md [skip ci] 2022-02-26 23:03:07 -08:00
Rhet Turnbull
c95f682ca6 Updated docs [skip ci] 2022-02-26 22:57:25 -08:00
Rhet Turnbull
d2753672f3 Fixed entry point 2022-02-26 22:49:39 -08:00
Rhet Turnbull
8ee4ea46c5 Updated CHANGELOG.md [skip ci] 2022-02-26 22:49:18 -08:00
Rhet Turnbull
6fae979061 Updated docs [skip ci] 2022-02-26 22:34:03 -08:00
Rhet Turnbull
25d6f148be CLI refactor (#642)
* Initial refactoring of cli.py

* Renamed cli_help

* Refactored all cli commands

* Dropped support for 3.7

* Added test for export with --min-size

* Version bump

* Fixed python version
2022-02-26 22:29:19 -08:00
Rhet Turnbull
3704fc4a23 Fixed 3.10 in yaml 2022-02-26 17:20:20 -08:00
Rhet Turnbull
7883fc1911 Dropped 3.7 2022-02-26 17:15:59 -08:00
Rhet Turnbull
29ff7f8666 Updated CHANGELOG.md [skip ci] 2022-02-26 16:51:51 -08:00
Rhet Turnbull
43e1cb18cc Updated tests 2022-02-26 15:30:13 -08:00
Rhet Turnbull
26f916e4cb Bug fix for bitmath types in saved config 2022-02-26 15:23:46 -08:00
Rhet Turnbull
4e9e877b27 Updated CHANGELOG.md [skip ci] 2022-02-24 05:23:49 -08:00
Rhet Turnbull
3a990e3997 Updated docs [skip ci] 2022-02-24 05:22:58 -08:00
Rhet Turnbull
4d1b1db2a7 Updated tested versions 2022-02-24 05:18:23 -08:00
Rhet Turnbull
173e3ccc37 Removed debug code from exiftool, fixed #641 2022-02-24 05:09:42 -08:00
Rhet Turnbull
9964fd0635 Updated comment for #636 2022-02-23 06:15:03 -08:00
Rhet Turnbull
e789cd5e9d pass PATH to exiftool to find xattr 2022-02-22 22:11:12 -08:00
Rhet Turnbull
6cb7dedd9b Updated debug info 2022-02-22 09:49:02 -08:00
Rhet Turnbull
39ba17dd1c Added debug output to exiftool 2022-02-22 06:40:25 -08:00
Rhet Turnbull
5b66962ac1 Fixed export of bursts with --uuid and --selected, #640 2022-02-21 22:58:54 -08:00
Rhet Turnbull
c05340f631 removed macos-12 as it doesn't work with Actions 2022-02-21 22:49:12 -08:00
Rhet Turnbull
f24c461cbb Added macos-12 2022-02-21 17:08:27 -08:00
Rhet Turnbull
c8ee679799 Added --sql command to exportdb 2022-02-21 16:06:16 -08:00
Rhet Turnbull
2966c9a60f Updated docs [skip ci] 2022-02-21 15:17:19 -08:00
Rhet Turnbull
acfcb0c49a Updated CHANGELOG.md [skip ci] 2022-02-21 11:39:04 -08:00
Rhet Turnbull
b92a681795 Added --ramdb option (#639) 2022-02-21 11:20:02 -08:00
Rhet Turnbull
1941e79d21 Updated CHANGELOG.md [skip ci] 2022-02-21 09:42:08 -08:00
Rhet Turnbull
5290fae2e0 Updated docs [skip ci] 2022-02-21 09:34:40 -08:00
Rhet Turnbull
ecbd370a47 Exportdb refactor (#638)
* Working on export_db refactor

* Added exportdb command, removed logic for missing export_db, #630

* Updated tests

* updated docs

* Added --config-only, #606

* Added validation for --exportdb

* Added --info to exportdb command

* Fixed exportdb --touch-file to migrate database if needed

* Added exportdb --migrate
2022-02-21 09:15:01 -08:00
Rhet Turnbull
d8204e65eb Allow multiple characters as path_sep, #634 2022-02-14 06:46:19 -08:00
Rhet Turnbull
9c26e5519b Added crash_reporter.py 2022-02-13 15:02:35 -08:00
Rhet Turnbull
060729c4c4 Added --debug and crash reporter to export, #628 2022-02-13 14:51:31 -08:00
Rhet Turnbull
65d51ab129 Updated docs [skip ci] 2022-02-13 00:21:27 -08:00
Rhet Turnbull
afbda030bc beta fix for #633, fix face regions in exiftool 2022-02-13 00:14:59 -08:00
Rhet Turnbull
d111d07fb7 Updated CHANGELOG.md [skip ci] 2022-02-12 21:13:56 -08:00
146 changed files with 8488 additions and 4923 deletions

View File

@@ -10,7 +10,7 @@ jobs:
max-parallel: 4
matrix:
os: [macos-10.15]
python-version: [3.7, 3.8, 3.9]
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v1

View File

@@ -4,6 +4,80 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.47.1](https://github.com/RhetTbull/osxphotos/compare/v0.47.0...v0.47.1)
> 26 February 2022
- Fixed entry point [`d275367`](https://github.com/RhetTbull/osxphotos/commit/d2753672f36a0c84b7be143a47cc85cd6c99cb6d)
#### [v0.47.0](https://github.com/RhetTbull/osxphotos/compare/v0.46.6...v0.47.0)
> 26 February 2022
- CLI refactor [`#642`](https://github.com/RhetTbull/osxphotos/pull/642)
- Updated docs [skip ci] [`6fae979`](https://github.com/RhetTbull/osxphotos/commit/6fae97906124c9284e382170e20c8ab9999105b0)
- Fixed 3.10 in yaml [`3704fc4`](https://github.com/RhetTbull/osxphotos/commit/3704fc4a23e83ff2d16d6d221fb6c752dabcedca)
- Dropped 3.7 [`7883fc1`](https://github.com/RhetTbull/osxphotos/commit/7883fc1911057df9a4c596375b498e85a73c1bec)
#### [v0.46.6](https://github.com/RhetTbull/osxphotos/compare/v0.46.5...v0.46.6)
> 26 February 2022
- Updated tests [`43e1cb1`](https://github.com/RhetTbull/osxphotos/commit/43e1cb18cc65b1abe1f49b464563e816b2ed1cff)
- Updated docs [skip ci] [`3a990e3`](https://github.com/RhetTbull/osxphotos/commit/3a990e39971d838e52d5f19bf28b8253c4c7b811)
- Bug fix for bitmath types in saved config [`26f916e`](https://github.com/RhetTbull/osxphotos/commit/26f916e4cbf4f28154c47aa2de1fdbc0aebc65b3)
#### [v0.46.5](https://github.com/RhetTbull/osxphotos/compare/v0.46.4...v0.46.5)
> 24 February 2022
- Updated tested versions [`4d1b1db`](https://github.com/RhetTbull/osxphotos/commit/4d1b1db2a7cf34afaa2dc5dbebc69021ff77964f)
#### [v0.46.4](https://github.com/RhetTbull/osxphotos/compare/v0.46.1...v0.46.4)
> 24 February 2022
- Removed debug code from exiftool, fixed #641 [`#641`](https://github.com/RhetTbull/osxphotos/issues/641)
- Added debug output to exiftool [`39ba17d`](https://github.com/RhetTbull/osxphotos/commit/39ba17dd1cb4d8a61ab4dc8d5cff12ff9871eee0)
- Fixed export of bursts with --uuid and --selected, #640 [`5b66962`](https://github.com/RhetTbull/osxphotos/commit/5b66962ac1bc1f48106fb8eeb600e6010088dc3b)
- Added --sql command to exportdb [`c8ee679`](https://github.com/RhetTbull/osxphotos/commit/c8ee6797999af954c32e96ac3799a19002f4f0fe)
- Updated docs [skip ci] [`2966c9a`](https://github.com/RhetTbull/osxphotos/commit/2966c9a60fc828afdf34263b759159a3ade31897)
- Updated debug info [`6cb7ded`](https://github.com/RhetTbull/osxphotos/commit/6cb7dedd9be53d2c62489125fc44b9f4dccfb7ae)
#### [v0.46.1](https://github.com/RhetTbull/osxphotos/compare/v0.46.0...v0.46.1)
> 21 February 2022
- Added --ramdb option [`#639`](https://github.com/RhetTbull/osxphotos/pull/639)
#### [v0.46.0](https://github.com/RhetTbull/osxphotos/compare/v0.45.12...v0.46.0)
> 21 February 2022
- Exportdb refactor [`#638`](https://github.com/RhetTbull/osxphotos/pull/638)
- Updated docs [skip ci] [`5290fae`](https://github.com/RhetTbull/osxphotos/commit/5290fae2e0ad062750348aedfee4feaf7b2e769f)
#### [v0.45.12](https://github.com/RhetTbull/osxphotos/compare/v0.45.11...v0.45.12)
> 14 February 2022
- Allow multiple characters as path_sep, #634 [`d8204e6`](https://github.com/RhetTbull/osxphotos/commit/d8204e65eb740cece468ef021cbdf45d896d954e)
- Added --debug and crash reporter to export, #628 [`060729c`](https://github.com/RhetTbull/osxphotos/commit/060729c4c4255651c6ee8149989d9de541d0a6aa)
- Added crash_reporter.py [`9c26e55`](https://github.com/RhetTbull/osxphotos/commit/9c26e5519b2d48f3a0ae80d1cc4a765c12b62d40)
#### [v0.45.11](https://github.com/RhetTbull/osxphotos/compare/v0.45.10...v0.45.11)
> 13 February 2022
- beta fix for #633, fix face regions in exiftool [`afbda03`](https://github.com/RhetTbull/osxphotos/commit/afbda030bce87f914445ebbced3f0e110e2e203b)
- Updated docs [skip ci] [`65d51ab`](https://github.com/RhetTbull/osxphotos/commit/65d51ab1290e7c7804021e24829b93f5dce81245)
#### [v0.45.10](https://github.com/RhetTbull/osxphotos/compare/v0.45.9...v0.45.10)
> 12 February 2022
- Added --force-update, #621 [`30abddd`](https://github.com/RhetTbull/osxphotos/commit/30abdddaf3765f1d604984d4781b78b7806871e1)
#### [v0.45.9](https://github.com/RhetTbull/osxphotos/compare/v0.45.8...v0.45.9)
> 12 February 2022

View File

@@ -68,7 +68,7 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through mac
This package will read Photos databases for any supported version on any supported macOS version. E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.
Requires python >= `3.7`.
Requires python >= `3.8`.
## Installation
@@ -1173,6 +1173,11 @@ Options:
'.osxphotos_export.db' in the export
directory. If --exportdb is specified, it
will be saved to the specified file.
--ramdb Copy export database to memory during export;
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.
--load-config <config file path>
Load options from file as written with --save-
config. This allows you to save a complex
@@ -1186,8 +1191,12 @@ Options:
corresponding values in the config file.
--save-config <config file path>
Save options to file for use with --load-
config. File format is TOML.
--help Show this message and exit.
config. File format is TOML. See also
--config-only.
--config-only If specified, saves the config file but does
not export any files; must be used with
--save-config.
-h, --help Show this message and exit.
** Export **
@@ -1732,7 +1741,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.45.10'
{osxphotos_version} The osxphotos version, e.g. '0.47.1'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -3636,7 +3645,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.45.10'|
|{osxphotos_version}|The osxphotos version, e.g. '0.47.1'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -3736,7 +3745,7 @@ Args:
Returns: ExportResults instance
*Note*: to use dry run mode, you must set options.dry_run=True and also pass in memory version of export_db, and no-op fileutil (e.g. ExportDBInMemory and FileUtilNoOp) in options.export_db and options.fileutil respectively.
*Note*: to use dry run mode, you must set options.dry_run=True and also pass in memory version of export_db, and no-op fileutil (e.g. `ExportDBInMemory` and `FileUtilNoOp`) in options.export_db and options.fileutil respectively.
#### `ExportOptions`
@@ -3752,7 +3761,7 @@ Attributes:
- exiftool_flags (list of str): 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
- export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
- export_db: (ExportDB_ABC): instance of a class that conforms to ExportDB_ABC 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
- fileutil: (FileUtilABC): class that conforms to FileUtilABC with various file utilities
- ignore_date_modified (bool): for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
- ignore_signature (bool, default=False): ignore file signature when used with update (look only at filename)

View File

@@ -23,7 +23,7 @@ If you have access to macOS 12 / Monterey beta and would like to help ensure osx
This package will read Photos databases for any supported version on any supported macOS version.
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.
Requires python >= ``3.7``.
Requires python >= ``3.8``.
Installation
------------

4
cli.py
View File

@@ -12,7 +12,7 @@
"""
from osxphotos.cli import cli
from osxphotos.cli.cli import cli_main
if __name__ == "__main__":
cli()
cli_main()

View File

@@ -1,10 +1,12 @@
build
m2r2
pdbpp
pyinstaller==4.4
pytest-mock
pytest==6.2.4
pytest==7.0.1
Sphinx
sphinx_click
sphinx_rtd_theme
sphinxcontrib-programoutput
twine
wheel
Sphinx
wheel

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 5bcb39b8d7265fb2c5780a64f97662ef
config: bc3dce8a14bcd1b0c8a34e4d16f0011f
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.43.9 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
@@ -31,11 +31,7 @@
<div class="body" role="main">
<h1>All modules for which code is available</h1>
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
<ul><li><a href="osxphotos/photoinfo.html">osxphotos.photoinfo</a></li>
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
</ul>
@@ -93,7 +89,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.43.8 documentation</title>
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.46.6 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -60,22 +60,28 @@
<span class="n">_PHOTO_TYPE</span><span class="p">,</span>
<span class="n">_PHOTOS_3_VERSION</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_TYPE_ALBUM</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_TYPE_PROJECT</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_TYPE_SLIDESHOW</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ROOT_FOLDER</span><span class="p">,</span>
<span class="n">_PHOTOS_4_TOP_LEVEL_ALBUM</span><span class="p">,</span>
<span class="n">_PHOTOS_4_TOP_LEVEL_ALBUMS</span><span class="p">,</span>
<span class="n">_PHOTOS_4_VERSION</span><span class="p">,</span>
<span class="n">_PHOTOS_5_ALBUM_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_FOLDER_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_PROJECT_ALBUM_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_ROOT_FOLDER_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span><span class="p">,</span>
<span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span>
<span class="n">_TESTED_OS_VERSIONS</span><span class="p">,</span>
<span class="n">_UNKNOWN_PERSON</span><span class="p">,</span>
<span class="n">BURST_KEY</span><span class="p">,</span>
<span class="n">BURST_PICK_TYPE_NONE</span><span class="p">,</span>
<span class="n">BURST_SELECTED</span><span class="p">,</span>
<span class="n">TIME_DELTA</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">.._version</span> <span class="kn">import</span> <span class="n">__version__</span>
<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="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">..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>
@@ -94,6 +100,8 @@
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">.photosdb_utils</span> <span class="kn">import</span> <span class="n">get_db_model_version</span><span class="p">,</span> <span class="n">get_db_version</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;PhotosDB&quot;</span><span class="p">]</span>
<span class="c1"># TODO: Add test for imageTimeZoneOffsetSeconds = None</span>
<span class="c1"># TODO: Add test for __str__</span>
<span class="c1"># TODO: Add special albums and magic albums</span>
@@ -462,7 +470,7 @@
<span class="k">for</span> <span class="n">folder</span><span class="p">,</span> <span class="n">detail</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfolder_details</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;intrash&quot;</span><span class="p">]</span>
<span class="ow">and</span> <span class="ow">not</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;isMagic&quot;</span><span class="p">]</span>
<span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;parentFolderUuid&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUM</span>
<span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;parentFolderUuid&quot;</span><span class="p">]</span> <span class="ow">in</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUMS</span>
<span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">folders</span> <span class="o">=</span> <span class="p">[</span>
@@ -483,7 +491,7 @@
<span class="k">for</span> <span class="n">folder</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfolder_details</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folder</span><span class="p">[</span><span class="s2">&quot;intrash&quot;</span><span class="p">]</span>
<span class="ow">and</span> <span class="ow">not</span> <span class="n">folder</span><span class="p">[</span><span class="s2">&quot;isMagic&quot;</span><span class="p">]</span>
<span class="ow">and</span> <span class="n">folder</span><span class="p">[</span><span class="s2">&quot;parentFolderUuid&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUM</span>
<span class="ow">and</span> <span class="n">folder</span><span class="p">[</span><span class="s2">&quot;parentFolderUuid&quot;</span><span class="p">]</span> <span class="ow">in</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUMS</span>
<span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">folder_names</span> <span class="o">=</span> <span class="p">[</span>
@@ -562,6 +570,18 @@
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_import_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">project_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return list of AlbumInfo projects for each project in the database&quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_project_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_project_info</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">ProjectInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="n">album</span><span class="p">)</span>
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_album_uuids</span><span class="p">(</span><span class="n">project</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_project_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">db_version</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return the database version as stored in LiGlobals table&quot;&quot;&quot;</span>
@@ -673,14 +693,18 @@
<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">pk</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">_UNKNOWN_PERSON</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
<span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
<span class="k">else</span> <span class="n">_UNKNOWN_PERSON</span>
<span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;pk&quot;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
<span class="s2">&quot;fullname&quot;</span><span class="p">:</span> <span class="n">fullname</span><span class="p">,</span>
<span class="s2">&quot;facecount&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span>
<span class="s2">&quot;keyface&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">5</span><span class="p">],</span>
<span class="s2">&quot;displayname&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span>
<span class="s2">&quot;displayname&quot;</span><span class="p">:</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">4</span><span class="p">]),</span>
<span class="s2">&quot;photo_uuid&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="s2">&quot;keyface_uuid&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">}</span>
@@ -747,13 +771,6 @@
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span> <span class="o">=</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Finished walking through persons&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_uuid</span><span class="p">))</span>
<span class="c1"># Get info on albums</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing albums.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -881,24 +898,15 @@
<span class="c1"># build folder hierarchy</span>
<span class="k">for</span> <span class="n">album</span><span class="p">,</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="n">parent_folder</span> <span class="o">=</span> <span class="n">details</span><span class="p">[</span><span class="s2">&quot;folderUuid&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="n">details</span><span class="p">[</span>
<span class="s2">&quot;albumSubclass&quot;</span>
<span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTOS_4_ALBUM_KIND</span> <span class="ow">and</span> <span class="n">parent_folder</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span>
<span class="n">_PHOTOS_4_TOP_LEVEL_ALBUM</span>
<span class="p">]:</span>
<span class="k">if</span> <span class="p">(</span>
<span class="n">details</span><span class="p">[</span><span class="s2">&quot;albumSubclass&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTOS_4_ALBUM_KIND</span>
<span class="ow">and</span> <span class="n">parent_folder</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUMS</span>
<span class="p">):</span>
<span class="n">folder_hierarchy</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_build_album_folder_hierarchy_4</span><span class="p">(</span><span class="n">parent_folder</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album</span><span class="p">]</span> <span class="o">=</span> <span class="n">folder_hierarchy</span>
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album</span><span class="p">]</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Finished walking through albums&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_album</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_uuid</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfolder_details</span><span class="p">))</span>
<span class="c1"># Get info on keywords</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing keywords.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -914,13 +922,16 @@
<span class="sd"> RKMaster.uuid = RKVersion.masterUuid</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="k">for</span> <span class="n">keyword_title</span><span class="p">,</span> <span class="n">keyword_uuid</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">keyword_title</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">keyword_title</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_title</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_uuid</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span>
<span class="c1"># Get info on disk volumes</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;select RKVolume.modelId, RKVolume.name from RKVolume&quot;</span><span class="p">)</span>
@@ -1042,13 +1053,11 @@
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</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">&quot;uuid = &#39;</span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, master = &#39;</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">&quot;</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="o">=</span> <span class="p">{}</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;_uuid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">uuid</span> <span class="c1"># stored here for easier debugging</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;modelID&quot;</span><span class="p">]</span> <span class="o">=</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">&quot;masterUuid&quot;</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">&quot;filename&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;filename&quot;</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">3</span><span class="p">])</span>
<span class="c1"># There are sometimes negative values for lastmodifieddate in the database</span>
<span class="c1"># I don&#39;t know what these mean but they will raise exception in datetime if</span>
@@ -1287,13 +1296,13 @@
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;volumeId&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;imagePath&quot;</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="n">info</span><span class="p">[</span><span class="s2">&quot;isMissing&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;originalFilename&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;originalFilename&quot;</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">4</span><span class="p">])</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;UTI&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;modelID&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;fileSize&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;isTrulyRAW&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;alternateMasterUuid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;filename&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;filename&quot;</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">10</span><span class="p">])</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_master</span><span class="p">[</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">info</span>
<span class="c1"># get details needed to find path of the edited photos</span>
@@ -1565,39 +1574,6 @@
<span class="c1"># done processing, dump debug data if requested</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Done processing details from Photos library.&quot;</span><span class="p">)</span>
<span class="k">if</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="s2">&quot;Faces (_dbfaces_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_uuid</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="s2">&quot;Persons (_dbpersons_pk):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</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="s2">&quot;Keywords by uuid (_dbkeywords_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</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="s2">&quot;Keywords by keyword (_dbkeywords_keywords):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Albums by uuid (_dbalbums_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_uuid</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="s2">&quot;Albums by album (_dbalbums_albums):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_album</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="s2">&quot;Album details (_dbalbum_details):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Album titles (_dbalbum_titles):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_titles</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="s2">&quot;Volumes (_dbvolumes):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbvolumes</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="s2">&quot;Photos (_dbphotos):&quot;</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">pformat</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">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Burst Photos (dbphotos_burst:&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_build_album_folder_hierarchy_4</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">,</span> <span class="n">folders</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;recursively build folder/album hierarchy</span>
@@ -1615,7 +1591,7 @@
<span class="k">if</span> <span class="n">parent_uuid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">folders</span>
<span class="k">if</span> <span class="n">parent_uuid</span> <span class="o">==</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUM</span><span class="p">:</span>
<span class="k">if</span> <span class="n">parent_uuid</span> <span class="ow">in</span> <span class="n">_PHOTOS_4_TOP_LEVEL_ALBUMS</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folders</span><span class="p">:</span>
<span class="c1"># this is a top-level folder with no sub-folders</span>
<span class="n">folders</span> <span class="o">=</span> <span class="p">{</span><span class="n">uuid</span><span class="p">:</span> <span class="kc">None</span><span class="p">}</span>
@@ -1688,7 +1664,7 @@
<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">pk</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
<span class="k">if</span> <span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&quot;&quot;</span> <span class="ow">and</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">else</span> <span class="n">_UNKNOWN_PERSON</span>
<span class="p">)</span>
@@ -1698,7 +1674,7 @@
<span class="s2">&quot;fullname&quot;</span><span class="p">:</span> <span class="n">fullname</span><span class="p">,</span>
<span class="s2">&quot;facecount&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span>
<span class="s2">&quot;keyface&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span>
<span class="s2">&quot;displayname&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">5</span><span class="p">],</span>
<span class="s2">&quot;displayname&quot;</span><span class="p">:</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">5</span><span class="p">]),</span>
<span class="s2">&quot;photo_uuid&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="s2">&quot;keyface_uuid&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">}</span>
@@ -1762,13 +1738,6 @@
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span> <span class="o">=</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Finished walking through persons&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_uuid</span><span class="p">))</span>
<span class="c1"># get details about albums</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing albums.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -1885,13 +1854,6 @@
<span class="c1"># shared albums can&#39;t be in folders</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album</span><span class="p">]</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Finished walking through albums&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_album</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_uuid</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">))</span>
<span class="c1"># get details on keywords</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing keywords.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -1901,29 +1863,22 @@
<span class="s2"> JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK </span>
<span class="s2"> JOIN ZKEYWORD ON ZKEYWORD.Z_PK = </span><span class="si">{</span><span class="n">keyword_join</span><span class="si">}</span><span class="s2"> &quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">keyword_title</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">keyword_title</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="k">if</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">&quot;Finished walking through keywords&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">))</span>
<span class="k">for</span> <span class="n">keyword_title</span><span class="p">,</span> <span class="n">keyword_uuid</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">keyword_title</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">keyword_title</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_title</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</span><span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_uuid</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword_title</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword_uuid</span><span class="p">]</span>
<span class="c1"># get details on disk volumes</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME&quot;</span><span class="p">)</span>
<span class="k">for</span> <span class="n">vol</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbvolumes</span><span class="p">[</span><span class="n">vol</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">=</span> <span class="n">vol</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="k">if</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">&quot;Finished walking through volumes&quot;</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="bp">self</span><span class="o">.</span><span class="n">_dbvolumes</span><span class="p">)</span>
<span class="c1"># get details about photos</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing photo details.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -2057,8 +2012,8 @@
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;hidden&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;favorite&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;originalFilename&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;filename&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;originalFilename&quot;</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">3</span><span class="p">])</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;filename&quot;</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">12</span><span class="p">])</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;directory&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span>
<span class="c1"># set latitude and longitude</span>
@@ -2534,50 +2489,7 @@
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing moments.&quot;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_process_moments</span><span class="p">()</span>
<span class="c1"># done processing, dump debug data if requested</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Done processing details from Photos library.&quot;</span><span class="p">)</span>
<span class="k">if</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="s2">&quot;Faces (_dbfaces_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_uuid</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="s2">&quot;Persons (_dbpersons_pk):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</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="s2">&quot;Keywords by uuid (_dbkeywords_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_uuid</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="s2">&quot;Keywords by keyword (_dbkeywords_keywords):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Albums by uuid (_dbalbums_uuid):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_uuid</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="s2">&quot;Albums by album (_dbalbums_albums):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_album</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="s2">&quot;Album details (_dbalbum_details):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">))</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Album titles (_dbalbum_titles):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_titles</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="s2">&quot;Album folders (_dbalbum_folders):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</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="s2">&quot;Album parent folders (_dbalbum_parent_folders):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_parent_folders</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="s2">&quot;Albums pk (_dbalbums_pk):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbalbums_pk</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="s2">&quot;Volumes (_dbvolumes):&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbvolumes</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="s2">&quot;Photos (_dbphotos):&quot;</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">pformat</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">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Burst Photos (dbphotos_burst:&quot;</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">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_process_moments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Process data from ZMOMENT table&quot;&quot;&quot;</span>
@@ -2638,8 +2550,8 @@
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;modificationDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;representativeDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;startDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;subtitle&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;subtitle&quot;</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">9</span><span class="p">])</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;title&quot;</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">10</span><span class="p">])</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;uuid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span>
<span class="c1"># if both lat/lon == -180, then it means location undefined</span>
@@ -2858,7 +2770,7 @@
<span class="n">hierarchy</span> <span class="o">=</span> <span class="n">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">)</span>
<span class="k">return</span> <span class="n">hierarchy</span>
<span class="k">def</span> <span class="nf">_get_album_uuids</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">shared</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">import_session</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">_get_album_uuids</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">shared</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">import_session</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">project</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return list of album UUIDs found in photos database</span>
<span class="sd"> Filters out albums in the trash and any special album types</span>
@@ -2866,20 +2778,21 @@
<span class="sd"> Args:</span>
<span class="sd"> shared: boolean; if True, returns shared albums, else normal albums</span>
<span class="sd"> import_session: boolean, if True, returns import session albums, else normal or shared albums</span>
<span class="sd"> project: boolean, if True, returns albums that are part of My Projects</span>
<span class="sd"> Note: flags (shared, import_session) are mutually exclusive</span>
<span class="sd"> Raises:</span>
<span class="sd"> ValueError: raised if mutually exclusive flags passed</span>
<span class="sd"> Returns: list of album UUIDs</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">shared</span> <span class="ow">and</span> <span class="n">import_session</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">bool</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">[</span><span class="n">shared</span><span class="p">,</span> <span class="n">import_session</span><span class="p">,</span> <span class="n">project</span><span class="p">])</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">&quot;flags are mutually exclusive: pass zero or one of shared, import_session&quot;</span>
<span class="s2">&quot;flags are mutually exclusive: pass zero or one of shared, import_session, projects&quot;</span>
<span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">version4</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">if</span> <span class="n">shared</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Shared albums not implemented for Photos library 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">&quot;</span>
@@ -2890,16 +2803,44 @@
<span class="sa">f</span><span class="s2">&quot;Import sessions not implemented for Photos library 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">&quot;</span>
<span class="p">)</span>
<span class="k">return</span> <span class="p">[]</span> <span class="c1"># not implemented for _PHOTOS_4_VERSION</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">elif</span> <span class="n">project</span><span class="p">:</span>
<span class="n">album_type</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">_PHOTOS_4_ALBUM_TYPE_PROJECT</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_TYPE_SLIDESHOW</span><span class="p">,</span>
<span class="p">]</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_4_ALBUM_KIND</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">version4</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">if</span> <span class="n">shared</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span>
<span class="k">elif</span> <span class="n">import_session</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_ALBUM_KIND</span>
<span class="n">album_type</span> <span class="o">=</span> <span class="p">[</span><span class="n">_PHOTOS_4_ALBUM_TYPE_ALBUM</span><span class="p">]</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_4_ALBUM_KIND</span>
<span class="n">album_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># look through _dbalbum_details because _dbalbums_album won&#39;t have empty albums it</span>
<span class="k">for</span> <span class="n">album</span><span class="p">,</span> <span class="n">detail</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="p">(</span>
<span class="n">detail</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">album_kind</span>
<span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;albumType&quot;</span><span class="p">]</span> <span class="ow">in</span> <span class="n">album_type</span>
<span class="ow">and</span> <span class="ow">not</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;intrash&quot;</span><span class="p">]</span>
<span class="ow">and</span> <span class="p">(</span>
<span class="p">(</span><span class="n">shared</span> <span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
<span class="ow">or</span> <span class="p">(</span><span class="ow">not</span> <span class="n">shared</span> <span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">)</span>
<span class="p">)</span>
<span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;folderUuid&quot;</span><span class="p">]</span> <span class="o">!=</span> <span class="n">_PHOTOS_4_ROOT_FOLDER</span>
<span class="c1"># in Photos &lt;= 4, special albums like &quot;printAlbum&quot; have kind _PHOTOS_4_ALBUM_KIND</span>
<span class="c1"># but should not be listed here; they can be distinguished by looking</span>
<span class="c1"># for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM</span>
<span class="p">):</span>
<span class="n">album_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">album</span><span class="p">)</span>
<span class="k">return</span> <span class="n">album_list</span>
<span class="c1"># Photos version 5+</span>
<span class="k">if</span> <span class="n">shared</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span>
<span class="k">elif</span> <span class="n">import_session</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND</span>
<span class="k">elif</span> <span class="n">project</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_PROJECT_ALBUM_KIND</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">album_kind</span> <span class="o">=</span> <span class="n">_PHOTOS_5_ALBUM_KIND</span>
<span class="n">album_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># look through _dbalbum_details because _dbalbums_album won&#39;t have empty albums it</span>
@@ -2911,13 +2852,6 @@
<span class="p">(</span><span class="n">shared</span> <span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
<span class="ow">or</span> <span class="p">(</span><span class="ow">not</span> <span class="n">shared</span> <span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">)</span>
<span class="p">)</span>
<span class="ow">and</span> <span class="p">(</span>
<span class="ow">not</span> <span class="n">version4</span>
<span class="c1"># in Photos 4, special albums like &quot;printAlbum&quot; have kind _PHOTOS_4_ALBUM_KIND</span>
<span class="c1"># but should not be listed here; they can be distinguished by looking</span>
<span class="c1"># for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM</span>
<span class="ow">or</span> <span class="p">(</span><span class="n">version4</span> <span class="ow">and</span> <span class="n">detail</span><span class="p">[</span><span class="s2">&quot;folderUuid&quot;</span><span class="p">]</span> <span class="o">!=</span> <span class="n">_PHOTOS_4_ROOT_FOLDER</span><span class="p">)</span>
<span class="p">)</span>
<span class="p">):</span>
<span class="n">album_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">album</span><span class="p">)</span>
<span class="k">return</span> <span class="n">album_list</span>
@@ -3020,6 +2954,7 @@
<span class="k">if</span> <span class="n">keywords</span><span class="p">:</span>
<span class="n">keyword_set</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
<span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
<span class="n">keyword</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">keyword</span><span class="p">)</span>
<span class="k">if</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
<span class="n">keyword_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">])</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_set</span><span class="p">)</span>
@@ -3027,6 +2962,7 @@
<span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person_set</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">)</span>
<span class="k">if</span> <span class="n">person</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</span><span class="p">:</span>
<span class="k">for</span> <span class="n">pk</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</span><span class="p">[</span><span class="n">person</span><span class="p">]:</span>
<span class="k">try</span><span class="p">:</span>
@@ -3058,6 +2994,7 @@
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">&quot;burst&quot;</span><span class="p">]</span> <span class="ow">and</span> <span class="ow">not</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</span> <span class="n">BURST_SELECTED</span>
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">&amp;</span> <span class="n">BURST_KEY</span>
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">][</span><span class="s2">&quot;burstPickType&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="n">BURST_PICK_TYPE_NONE</span>
<span class="p">):</span>
<span class="c1"># not a key/selected burst photo, don&#39;t include in returned results</span>
<span class="k">continue</span>
@@ -3068,8 +3005,6 @@
<span class="p">):</span>
<span class="n">info</span> <span class="o">=</span> <span class="n">PhotoInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="n">p</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">p</span><span class="p">])</span>
<span class="n">photoinfo</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">info</span><span class="p">)</span>
<span class="k">if</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">&quot;photoinfo: </span><span class="si">{</span><span class="n">pformat</span><span class="p">(</span><span class="n">photoinfo</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="n">photoinfo</span></div>
@@ -3377,27 +3312,6 @@
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">to_time</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">&lt;=</span> <span class="n">options</span><span class="o">.</span><span class="n">to_time</span><span class="p">]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="c1"># add the burst_photos to the export set</span>
<span class="n">photos_burst</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">burst</span><span class="p">]</span>
<span class="k">for</span> <span class="n">burst</span> <span class="ow">in</span> <span class="n">photos_burst</span><span class="p">:</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">missing_bursts</span><span class="p">:</span>
<span class="c1"># include burst photos that are missing</span>
<span class="n">photos</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">burst</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># don&#39;t include missing burst images (these can&#39;t be downloaded with AppleScript)</span>
<span class="n">photos</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">burst</span><span class="o">.</span><span class="n">burst_photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">])</span>
<span class="c1"># remove duplicates as each burst photo in the set that&#39;s selected would</span>
<span class="c1"># result in the entire set being added above</span>
<span class="c1"># can&#39;t use set() because PhotoInfo not hashable</span>
<span class="n">seen_uuids</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">in</span> <span class="n">seen_uuids</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">seen_uuids</span><span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span>
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">seen_uuids</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
<span class="k">if</span> <span class="n">name</span><span class="p">:</span>
<span class="c1"># search filename fields for text</span>
<span class="c1"># if more than one, find photos with all title values in filename</span>
@@ -3406,23 +3320,35 @@
<span class="c1"># case-insensitive</span>
<span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="n">n</span> <span class="o">=</span> <span class="n">n</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span>
<span class="n">p</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span>
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&gt;=</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
<span class="c1"># search only original_filename (#594)</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()]</span>
<span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span>
<span class="n">p</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span>
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span>
<span class="n">p</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span>
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span> <span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&gt;=</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
<span class="c1"># search only original_filename (#594)</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span>
<span class="p">[</span>
<span class="n">p</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span>
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span> <span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">min_size</span><span class="p">:</span>
@@ -3536,6 +3462,28 @@
<span class="k">for</span> <span class="n">function</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">function</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">photos</span><span class="p">)</span>
<span class="c1"># burst should be checked last, ref #640</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="c1"># add the burst_photos to the export set</span>
<span class="n">photos_burst</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">burst</span><span class="p">]</span>
<span class="k">for</span> <span class="n">burst</span> <span class="ow">in</span> <span class="n">photos_burst</span><span class="p">:</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">missing_bursts</span><span class="p">:</span>
<span class="c1"># include burst photos that are missing</span>
<span class="n">photos</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">burst</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># don&#39;t include missing burst images (these can&#39;t be downloaded with AppleScript)</span>
<span class="n">photos</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">burst</span><span class="o">.</span><span class="n">burst_photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">])</span>
<span class="c1"># remove duplicates as each burst photo in the set that&#39;s selected would</span>
<span class="c1"># result in the entire set being added above</span>
<span class="c1"># can&#39;t use set() because PhotoInfo not hashable</span>
<span class="n">seen_uuids</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">in</span> <span class="n">seen_uuids</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">seen_uuids</span><span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span>
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">seen_uuids</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
<span class="k">return</span> <span class="n">photos</span></div>
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span>
@@ -3659,7 +3607,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -3,4 +3,6 @@ osxphotos command line interface (CLI)
.. click:: osxphotos.cli:cli
:prog: osxphotos
:nested: full
:nested: full
.. program-output:: python3 -m osxphotos --help

View File

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

View File

@@ -6,7 +6,7 @@
<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/" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.45.10 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.45.10 documentation</title>
<title>Index &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -34,8 +34,444 @@
<h1 id="index">Index</h1>
<div class="genindex-jumpbox">
<a href="#A"><strong>A</strong></a>
| <a href="#B"><strong>B</strong></a>
| <a href="#C"><strong>C</strong></a>
| <a href="#D"><strong>D</strong></a>
| <a href="#E"><strong>E</strong></a>
| <a href="#F"><strong>F</strong></a>
| <a href="#G"><strong>G</strong></a>
| <a href="#H"><strong>H</strong></a>
| <a href="#I"><strong>I</strong></a>
| <a href="#J"><strong>J</strong></a>
| <a href="#K"><strong>K</strong></a>
| <a href="#L"><strong>L</strong></a>
| <a href="#M"><strong>M</strong></a>
| <a href="#O"><strong>O</strong></a>
| <a href="#P"><strong>P</strong></a>
| <a href="#Q"><strong>Q</strong></a>
| <a href="#R"><strong>R</strong></a>
| <a href="#S"><strong>S</strong></a>
| <a href="#T"><strong>T</strong></a>
| <a href="#U"><strong>U</strong></a>
| <a href="#V"><strong>V</strong></a>
| <a href="#W"><strong>W</strong></a>
</div>
<h2 id="A">A</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.adjustments">adjustments (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.album_info">album_info (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.album_info">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotosDB.album_info_shared">album_info_shared (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.albums">albums (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.albums">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared">albums_shared (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared_as_dict">albums_shared_as_dict (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.asdict">asdict() (osxphotos.PhotoInfo method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="B">B</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_default_pick">burst_default_pick (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_key">burst_key (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_selected">burst_selected (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="C">C</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.comments">comments (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="D">D</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.date">date (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.date_added">date_added (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.date_modified">date_modified (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.date_trashed">date_trashed (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.db_path">db_path (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.db_version">db_version (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.description">description (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.detected_text">detected_text() (osxphotos.PhotoInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.duplicates">duplicates (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="E">E</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.execute">execute() (osxphotos.PhotosDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.exif_info">exif_info (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.exiftool">exiftool (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.export">export() (osxphotos.PhotoInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.external_edit">external_edit (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="F">F</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.face_info">face_info (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.favorite">favorite (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.filename">filename (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.folder_info">folder_info (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.folders">folders (osxphotos.PhotosDB property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="G">G</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.get_db_connection">get_db_connection() (osxphotos.PhotosDB method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.get_photo">get_photo() (osxphotos.PhotosDB method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="H">H</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.has_raw">has_raw (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.hasadjustments">hasadjustments (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.hdr">hdr (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.height">height (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.hidden">hidden (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="I">I</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.import_info">import_info (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.import_info">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotoInfo.incloud">incloud (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.intrash">intrash (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.iscloudasset">iscloudasset (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.ismissing">ismissing (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.ismovie">ismovie (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.isphoto">isphoto (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.israw">israw (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.isreference">isreference (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="J">J</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.json">json() (osxphotos.PhotoInfo method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="K">K</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.keywords">keywords (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.keywords">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.keywords_as_dict">keywords_as_dict (osxphotos.PhotosDB property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="L">L</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.labels">labels (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.labels">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotosDB.labels_as_dict">labels_as_dict (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.labels_normalized">labels_normalized (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.labels_normalized">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.labels_normalized_as_dict">labels_normalized_as_dict (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.library_path">library_path (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.likes">likes (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.live_photo">live_photo (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.location">location (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="M">M</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.moment">moment (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="O">O</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.orientation">orientation (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.original_filename">original_filename (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.original_filesize">original_filesize (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.original_height">original_height (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.original_orientation">original_orientation (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.original_width">original_width (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.owner">owner (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="P">P</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.panorama">panorama (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path">path (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_derivatives">path_derivatives (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_edited">path_edited (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_edited_live_photo">path_edited_live_photo (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_live_photo">path_live_photo (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.path_raw">path_raw (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.person_info">person_info (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.person_info">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotoInfo.persons">persons (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.persons">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.persons_as_dict">persons_as_dict (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo">PhotoInfo (class in osxphotos)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.photos">photos() (osxphotos.PhotosDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.photos_by_uuid">photos_by_uuid() (osxphotos.PhotosDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB">PhotosDB (class in osxphotos)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.place">place (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.portrait">portrait (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.project_info">project_info (osxphotos.PhotoInfo property)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotosDB.project_info">(osxphotos.PhotosDB property)</a>
</li>
</ul></li>
</ul></td>
</tr></table>
<h2 id="Q">Q</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.query">query() (osxphotos.PhotosDB method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="R">R</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.raw_original">raw_original (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.render_template">render_template() (osxphotos.PhotoInfo method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="S">S</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.score">score (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.screenshot">screenshot (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.search_info">search_info (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.search_info_normalized">search_info_normalized (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.selfie">selfie (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.shared">shared (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.slow_mo">slow_mo (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="T">T</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.time_lapse">time_lapse (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.title">title (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.tzoffset">tzoffset (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="U">U</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.uti">uti (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.uti_edited">uti_edited (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.uti_original">uti_original (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.uti_raw">uti_raw (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.uuid">uuid (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="V">V</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.visible">visible (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
<h2 id="W">W</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.width">width (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
</div>

View File

@@ -6,7 +6,7 @@
<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/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.45.10 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -50,7 +50,7 @@ You can also easily export both the original and edited photos.</p>
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
<p>This package will read Photos databases for any supported version on any supported macOS version.
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
<p>Requires python &gt;= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
<p>Requires python &gt;= <code class="docutils literal notranslate"><span class="pre">3.8</span></code>.</p>
</section>
<section id="installation">
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline"></a></h2>

View File

@@ -6,7 +6,7 @@
<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/" />
<title>osxphotos &#8212; osxphotos 0.45.10 documentation</title>
<title>osxphotos &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

Binary file not shown.

View File

@@ -6,7 +6,7 @@
<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/" />
<title>osxphotos package &#8212; osxphotos 0.45.10 documentation</title>
<title>osxphotos package &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -36,6 +36,883 @@
<h1>osxphotos package<a class="headerlink" href="#osxphotos-package" title="Permalink to this headline"></a></h1>
<section id="osxphotos-module">
<h2>osxphotos module<a class="headerlink" href="#osxphotos-module" title="Permalink to this headline"></a></h2>
<dl class="py class">
<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>
<dd><p>Processes a Photos.app library database to extract information about photos</p>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.album_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">album_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.album_info" title="Permalink to this definition"></a></dt>
<dd><p>return list of AlbumInfo objects for each album in the photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.album_info_shared">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">album_info_shared</span></span><a class="headerlink" href="#osxphotos.PhotosDB.album_info_shared" title="Permalink to this definition"></a></dt>
<dd><p>return list of AlbumInfo objects for each shared album in the photos database
only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.albums">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">albums</span></span><a class="headerlink" href="#osxphotos.PhotosDB.albums" title="Permalink to this definition"></a></dt>
<dd><p>return list of albums found in photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.albums_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">albums_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.albums_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>return albums as dict of albums, count in reverse sorted order (descending)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.albums_shared">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">albums_shared</span></span><a class="headerlink" href="#osxphotos.PhotosDB.albums_shared" title="Permalink to this definition"></a></dt>
<dd><p>return list of shared albums found in photos database
only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.albums_shared_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">albums_shared_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.albums_shared_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>returns shared albums as dict of albums, count in reverse sorted order (descending)
valid only on Photos 5; on Photos &lt;= 4, prints warning and returns empty dict</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.db_path">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">db_path</span></span><a class="headerlink" href="#osxphotos.PhotosDB.db_path" title="Permalink to this definition"></a></dt>
<dd><p>returns path to the Photos library database PhotosDB was initialized with</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.db_version">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">db_version</span></span><a class="headerlink" href="#osxphotos.PhotosDB.db_version" title="Permalink to this definition"></a></dt>
<dd><p>return the database version as stored in LiGlobals table</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.execute">
<span class="sig-name descname"><span class="pre">execute</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">sql</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.execute"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.execute" title="Permalink to this definition"></a></dt>
<dd><p>Execute sql statement and return cursor</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.folder_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">folder_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.folder_info" title="Permalink to this definition"></a></dt>
<dd><p>return list FolderInfo objects representing top-level folders in the photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.folders">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">folders</span></span><a class="headerlink" href="#osxphotos.PhotosDB.folders" title="Permalink to this definition"></a></dt>
<dd><p>return list of top-level folder names in the photos database</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.get_db_connection">
<span class="sig-name descname"><span class="pre">get_db_connection</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.get_db_connection"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.get_db_connection" title="Permalink to this definition"></a></dt>
<dd><p>Get connection to the working copy of the Photos database</p>
<dl class="field-list simple">
<dt class="field-odd">Returns</dt>
<dd class="field-odd"><p>tuple of (connection, cursor) to sqlite3 database</p>
</dd>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.get_photo">
<span class="sig-name descname"><span class="pre">get_photo</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.get_photo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.get_photo" title="Permalink to this definition"></a></dt>
<dd><p>Returns a single photo matching uuid</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>uuid</strong> the UUID of photo to get</p>
</dd>
<dt class="field-even">Returns</dt>
<dd class="field-even"><p>PhotoInfo instance for photo with UUID matching uuid or None if no match</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.import_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">import_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.import_info" title="Permalink to this definition"></a></dt>
<dd><p>return list of ImportInfo objects for each import session in the database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.keywords">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">keywords</span></span><a class="headerlink" href="#osxphotos.PhotosDB.keywords" title="Permalink to this definition"></a></dt>
<dd><p>return list of keywords found in photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.keywords_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">keywords_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.keywords_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>return keywords as dict of keyword, count in reverse sorted order (descending)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.labels">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels</span></span><a class="headerlink" href="#osxphotos.PhotosDB.labels" title="Permalink to this definition"></a></dt>
<dd><p>return list of all search info labels found in the library</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.labels_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.labels_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>count in reverse sorted order (descending)</p>
<dl class="field-list simple">
<dt class="field-odd">Type</dt>
<dd class="field-odd"><p>return labels as dict of label</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.labels_normalized">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels_normalized</span></span><a class="headerlink" href="#osxphotos.PhotosDB.labels_normalized" title="Permalink to this definition"></a></dt>
<dd><p>return list of all normalized search info labels found in the library</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.labels_normalized_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels_normalized_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.labels_normalized_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>count in reverse sorted order (descending)</p>
<dl class="field-list simple">
<dt class="field-odd">Type</dt>
<dd class="field-odd"><p>return normalized labels as dict of label</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.library_path">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">library_path</span></span><a class="headerlink" href="#osxphotos.PhotosDB.library_path" title="Permalink to this definition"></a></dt>
<dd><p>returns path to the Photos library PhotosDB was initialized with</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.person_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">person_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.person_info" title="Permalink to this definition"></a></dt>
<dd><p>return list of PersonInfo objects for each person in the photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.persons">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">persons</span></span><a class="headerlink" href="#osxphotos.PhotosDB.persons" title="Permalink to this definition"></a></dt>
<dd><p>return list of persons found in photos database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.persons_as_dict">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">persons_as_dict</span></span><a class="headerlink" href="#osxphotos.PhotosDB.persons_as_dict" title="Permalink to this definition"></a></dt>
<dd><p>return persons as dict of person, count in reverse sorted order (descending)</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.photos">
<span class="sig-name descname"><span class="pre">photos</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">keywords</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">uuid</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">persons</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">albums</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">images</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">movies</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_date</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">to_date</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">intrash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.photos"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.photos" title="Permalink to this definition"></a></dt>
<dd><p>Return a list of PhotoInfo objects
If called with no args, returns the entire database of photos
If called with args, returns photos matching the args (e.g. keywords, persons, etc.)
If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons)
If more than one keyword, uuid, persons, albums is passed, they are treated as “OR” criteria
e.g. keywords=[“wedding”,”vacation”] returns photos matching either keyword
from_date and to_date may be either naive or timezone-aware datetime.datetime objects.
If naive, timezone will be assumed to be local timezone.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>keywords</strong> list of keywords to search for</p></li>
<li><p><strong>uuid</strong> list of UUIDs to search for</p></li>
<li><p><strong>persons</strong> list of persons to search for</p></li>
<li><p><strong>albums</strong> list of album names to search for</p></li>
<li><p><strong>images</strong> if True, returns image files, if False, does not return images; default is True</p></li>
<li><p><strong>movies</strong> if True, returns movie files, if False, does not return movies; default is True</p></li>
<li><p><strong>from_date</strong> return photos with creation date &gt;= from_date (datetime.datetime object, default None)</p></li>
<li><p><strong>to_date</strong> return photos with creation date &lt;= to_date (datetime.datetime object, default None)</p></li>
<li><p><strong>intrash</strong> if True, returns only images in “Recently deleted items” folder,
if False returns only photos that arent deleted; default is False</p></li>
</ul>
</dd>
<dt class="field-even">Returns</dt>
<dd class="field-even"><p>list of PhotoInfo objects</p>
</dd>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.photos_by_uuid">
<span class="sig-name descname"><span class="pre">photos_by_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuids</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.photos_by_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.photos_by_uuid" title="Permalink to this definition"></a></dt>
<dd><dl class="simple">
<dt>Returns a list of photos with UUID in uuids.</dt><dd><p>Does not generate error if invalid or missing UUID passed.
This is faster than using PhotosDB.photos if you have list of UUIDs.
Returns photos regardless of intrash state.</p>
</dd>
</dl>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>uuid</strong> list of UUIDs of photos to get</p>
</dd>
<dt class="field-even">Returns</dt>
<dd class="field-even"><p>list of PhotoInfo instance for photo with UUID matching uuid or [] if no match</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.project_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">project_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.project_info" title="Permalink to this definition"></a></dt>
<dd><p>return list of AlbumInfo projects for each project in the database</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.query">
<span class="sig-name descname"><span class="pre">query</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">options</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">osxphotos.queryoptions.QueryOptions</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">List</span><span class="p"><span class="pre">[</span></span><a class="reference internal" href="#osxphotos.PhotoInfo" title="osxphotos.photoinfo.PhotoInfo"><span class="pre">osxphotos.photoinfo.PhotoInfo</span></a><span class="p"><span class="pre">]</span></span></span></span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.query"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.query" title="Permalink to this definition"></a></dt>
<dd><p>Run a query against PhotosDB to extract the photos based on user supplied options</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>options</strong> a QueryOptions instance</p>
</dd>
</dl>
</dd></dl>
</dd></dl>
<dl class="py class">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo">
<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">PhotoInfo</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">db</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">uuid</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">info</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/photoinfo.html#PhotoInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo" title="Permalink to this definition"></a></dt>
<dd><p>Info about a specific photo, contains all the details about the photo
including keywords, persons, albums, uuid, path, etc.</p>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.adjustments">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">adjustments</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.adjustments" title="Permalink to this definition"></a></dt>
<dd><p>Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.album_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">album_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.album_info" title="Permalink to this definition"></a></dt>
<dd><p>list of AlbumInfo objects representing albums the photo is contained in</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.albums">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">albums</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.albums" title="Permalink to this definition"></a></dt>
<dd><p>list of albums picture is contained in</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.asdict">
<span class="sig-name descname"><span class="pre">asdict</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo.html#PhotoInfo.asdict"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.asdict" title="Permalink to this definition"></a></dt>
<dd><p>return dict representation</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is part of a Burst photo set, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_album_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_album_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_album_info" title="Permalink to this definition"></a></dt>
<dd><p>If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info.</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_albums">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_albums</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_albums" title="Permalink to this definition"></a></dt>
<dd><p>If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_default_pick">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_default_pick</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_default_pick" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a burst image and is the photo that Photos selected as the default image for the burst set, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_key">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_key</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_key" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a burst photo and is the key image for the burst set (the image that Photos shows on top of the burst stack), otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_photos">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_photos</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_photos" title="Permalink to this definition"></a></dt>
<dd><p>If photo is a burst photo, returns list of PhotoInfo objects
that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.burst_selected">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">burst_selected</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.burst_selected" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.comments">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">comments</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.comments" title="Permalink to this definition"></a></dt>
<dd><p>Returns list of Comment objects for any comments on the photo (sorted by date)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.date">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">date</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.date" title="Permalink to this definition"></a></dt>
<dd><p>image creation date as timezone aware datetime object</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.date_added">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">date_added</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.date_added" title="Permalink to this definition"></a></dt>
<dd><p>Date photo was added to the database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.date_modified">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">date_modified</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.date_modified" title="Permalink to this definition"></a></dt>
<dd><p>image modification date as timezone aware datetime object
or None if no modification date set</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.date_trashed">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">date_trashed</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.date_trashed" title="Permalink to this definition"></a></dt>
<dd><p>Date asset was placed in the trash or None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.description">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">description</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.description" title="Permalink to this definition"></a></dt>
<dd><p>long / extended description of picture</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.detected_text">
<span class="sig-name descname"><span class="pre">detected_text</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">confidence_threshold</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">0.75</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo.html#PhotoInfo.detected_text"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.detected_text" title="Permalink to this definition"></a></dt>
<dd><p>Detects text in photo and returns lists of results as (detected text, confidence)</p>
<p>confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,
text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD</p>
<p>If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available</p>
<p>Returns: list of (detected text, confidence) tuples</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.duplicates">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">duplicates</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.duplicates" title="Permalink to this definition"></a></dt>
<dd><p>return list of PhotoInfo objects for possible duplicates (matching signature of original size, date, height, width) or empty list if no matching duplicates</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.exif_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">exif_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.exif_info" title="Permalink to this definition"></a></dt>
<dd><p>Returns an ExifInfo object with the EXIF data for photo
Note: the returned EXIF data is the data Photos stores in the database on import;
ExifInfo does not provide access to the EXIF info in the actual image file
Some or all of the fields may be None
Only valid for Photos 5; on earlier database returns None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.exiftool">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">exiftool</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.exiftool" title="Permalink to this definition"></a></dt>
<dd><p>Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
Requires that exiftool (<a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>) be installed
If exiftool not installed, logs warning and returns None
If photo path is missing, returns None</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.export">
<span class="sig-name descname"><span class="pre">export</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">dest</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">filename</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">edited</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">live_photo</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">raw_photo</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">export_as_hardlink</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">overwrite</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">increment</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">sidecar_json</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">sidecar_exiftool</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">sidecar_xmp</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_photos_export</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">timeout</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">120</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">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_albums_as_keywords</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_persons_as_keywords</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">keyword_template</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">description_template</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">render_options</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Optional</span><span class="p"><span class="pre">[</span></span><span class="pre">osxphotos.phototemplate.RenderOptions</span><span class="p"><span class="pre">]</span></span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo.html#PhotoInfo.export"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.export" title="Permalink to this definition"></a></dt>
<dd><p>export photo
dest: must be valid destination path (or exception raised)
filename: (optional): name of exported picture; if not provided, will use current filename</p>
<blockquote>
<div><p><strong>NOTE</strong>: if provided, user must ensure file extension (suffix) is correct.
For example, if photo is .CR2 file, edited image may be .jpeg.
If you provide an extension different than what the actual file is,
export will print a warning but will export the photo using the
incorrect file extension (unless use_photos_export is true, in which case export will
use the extension provided by Photos upon export; in this case, an incorrect extension is
silently ignored).
e.g. to get the extension of the edited photo,
reference PhotoInfo.path_edited</p>
</div></blockquote>
<dl class="simple">
<dt>edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version</dt><dd><p>(or raise exception if no edited version)</p>
</dd>
</dl>
<p>live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
overwrite: (boolean, default=False); if True will overwrite files if they already exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found</p>
<blockquote>
<div><p>if overwrite=False and increment=False, export will fail if destination file already exists</p>
</div></blockquote>
<dl class="simple">
<dt>sidecar_json: if set will write a json sidecar with data in format readable by exiftool</dt><dd><p>sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. <cite>exiftool -G -j</cite>)</p>
</dd>
<dt>sidecar_exiftool: if set will write a json sidecar with data in format readable by exiftool</dt><dd><p>sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. <cite>exiftool -j</cite>)</p>
</dd>
<dt>sidecar_xmp: if set will write an XMP sidecar with IPTC data</dt><dd><p>sidecar filename will be dest/filename.xmp</p>
</dd>
</dl>
<p>use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
returns list of full paths to the exported files
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
when exporting metadata with exiftool or sidecar
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
when exporting metadata with exiftool or sidecar
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
description_template: string; optional template string that will be rendered for use as photo description
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer</p>
<dl class="field-list simple">
<dt class="field-odd">Returns</dt>
<dd class="field-odd"><p>list of photos exported</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.external_edit">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">external_edit</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.external_edit" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if picture was edited outside of Photos using external editor</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.face_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">face_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.face_info" title="Permalink to this definition"></a></dt>
<dd><p>list of FaceInfo objects for faces in picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.favorite">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">favorite</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.favorite" title="Permalink to this definition"></a></dt>
<dd><p>True if picture is marked as favorite</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.filename">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">filename</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.filename" title="Permalink to this definition"></a></dt>
<dd><p>filename of the picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.has_raw">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">has_raw</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.has_raw" title="Permalink to this definition"></a></dt>
<dd><p>returns True if photo has an associated raw image (that is, its a RAW+JPEG pair), otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.hasadjustments">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">hasadjustments</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.hasadjustments" title="Permalink to this definition"></a></dt>
<dd><p>True if picture has adjustments / edits</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.hdr">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">hdr</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.hdr" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is an HDR photo, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.height">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">height</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.height" title="Permalink to this definition"></a></dt>
<dd><p>returns height of the current photo version in pixels</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.hidden">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">hidden</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.hidden" title="Permalink to this definition"></a></dt>
<dd><p>True if picture is hidden</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.import_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">import_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.import_info" title="Permalink to this definition"></a></dt>
<dd><p>ImportInfo object representing import session for the photo or None if no import session</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.incloud">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">incloud</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.incloud" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is cloud asset and is synched to cloud
False if photo is cloud asset and not yet synched to cloud
None if photo is not cloud asset</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.intrash">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">intrash</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.intrash" title="Permalink to this definition"></a></dt>
<dd><p>True if picture is in trash (Recently Deleted folder)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.iscloudasset">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">iscloudasset</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.iscloudasset" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a cloud asset (in an iCloud library),
otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.ismissing">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">ismissing</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.ismissing" title="Permalink to this definition"></a></dt>
<dd><p>returns true if photo is missing from disk (which means its not been downloaded from iCloud)</p>
<dl class="simple">
<dt>NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos</dt><dd><p>do not immediately get written to disk. In particular, Ive noticed that downloading
an image from the cloud does not force the database to be updated until something else
e.g. an edit, keyword, etc. occurs forcing a database synch
The exact process / timing is a mystery to be but be aware that if some photos were recently
downloaded from cloud to local storate their status in the database might still show
isMissing = 1</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.ismovie">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">ismovie</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.ismovie" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if file is a movie, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.isphoto">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">isphoto</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.isphoto" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if file is an image, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.israw">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">israw</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.israw" title="Permalink to this definition"></a></dt>
<dd><p>returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.isreference">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">isreference</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.isreference" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a reference (not copied to the Photos library), otherwise False</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.json">
<span class="sig-name descname"><span class="pre">json</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo.html#PhotoInfo.json"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.json" title="Permalink to this definition"></a></dt>
<dd><p>Return JSON representation</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.keywords">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">keywords</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.keywords" title="Permalink to this definition"></a></dt>
<dd><p>list of keywords for picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.labels">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.labels" title="Permalink to this definition"></a></dt>
<dd><p>returns list of labels applied to photo by Photos image categorization
only valid on Photos 5, on older libraries returns empty list</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.labels_normalized">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">labels_normalized</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.labels_normalized" title="Permalink to this definition"></a></dt>
<dd><p>returns normalized list of labels applied to photo by Photos image categorization
only valid on Photos 5, on older libraries returns empty list</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.likes">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">likes</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.likes" title="Permalink to this definition"></a></dt>
<dd><p>Returns list of Like objects for any likes on the photo (sorted by date)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.live_photo">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">live_photo</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.live_photo" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a live photo, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.location">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">location</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.location" title="Permalink to this definition"></a></dt>
<dd><p>returns (latitude, longitude) as float in degrees or None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.moment">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">moment</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.moment" title="Permalink to this definition"></a></dt>
<dd><p>Moment photo belongs to</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.orientation">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">orientation</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.orientation" title="Permalink to this definition"></a></dt>
<dd><p>returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.original_filename">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">original_filename</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.original_filename" title="Permalink to this definition"></a></dt>
<dd><p>original filename of the picture
Photos 5 mangles filenames upon import</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.original_filesize">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">original_filesize</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.original_filesize" title="Permalink to this definition"></a></dt>
<dd><p>returns filesize of original photo in bytes as int</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.original_height">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">original_height</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.original_height" title="Permalink to this definition"></a></dt>
<dd><p>returns height of the original photo version in pixels</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.original_orientation">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">original_orientation</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.original_orientation" title="Permalink to this definition"></a></dt>
<dd><p>returns EXIF orientation of the original photo version as int</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.original_width">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">original_width</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.original_width" title="Permalink to this definition"></a></dt>
<dd><p>returns width of the original photo version in pixels</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.owner">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">owner</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.owner" title="Permalink to this definition"></a></dt>
<dd><p>Return name of photo owner for shared photos (Photos 5+ only), or None if not shared</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.panorama">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">panorama</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.panorama" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a panorama, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path" title="Permalink to this definition"></a></dt>
<dd><p>absolute path on disk of the original picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path_derivatives">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path_derivatives</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path_derivatives" title="Permalink to this definition"></a></dt>
<dd><p>Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first)</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path_edited">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path_edited</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path_edited" title="Permalink to this definition"></a></dt>
<dd><p>absolute path on disk of the edited picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path_edited_live_photo">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path_edited_live_photo</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path_edited_live_photo" title="Permalink to this definition"></a></dt>
<dd><p>return path to edited version of live photo movie; only valid for Photos 5+</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path_live_photo">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path_live_photo</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path_live_photo" title="Permalink to this definition"></a></dt>
<dd><p>Returns path to the associated video file for a live photo
If photo is not a live photo, returns None
If photo is missing, returns None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.path_raw">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path_raw</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.path_raw" title="Permalink to this definition"></a></dt>
<dd><p>absolute path of associated RAW image or None if there is not one</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.person_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">person_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.person_info" title="Permalink to this definition"></a></dt>
<dd><p>list of PersonInfo objects for person in picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.persons">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">persons</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.persons" title="Permalink to this definition"></a></dt>
<dd><p>list of persons in picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.place">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">place</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.place" title="Permalink to this definition"></a></dt>
<dd><p>Returns PlaceInfo object containing reverse geolocation info</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.portrait">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">portrait</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.portrait" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a portrait, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.project_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">project_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.project_info" title="Permalink to this definition"></a></dt>
<dd><p>list of AlbumInfo objects representing projects for the photo or None if no projects</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.raw_original">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">raw_original</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.raw_original" title="Permalink to this definition"></a></dt>
<dd><p>returns True if associated raw image and the raw image is selected in Photos
via “Use RAW as Original ”
otherwise returns False</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.render_template">
<span class="sig-name descname"><span class="pre">render_template</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">template_str</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">options</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Optional</span><span class="p"><span class="pre">[</span></span><span class="pre">osxphotos.phototemplate.RenderOptions</span><span class="p"><span class="pre">]</span></span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo.html#PhotoInfo.render_template"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.render_template" title="Permalink to this definition"></a></dt>
<dd><p>Renders a template string for PhotoInfo instance using PhotoTemplate</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>template_str</strong> a template string with fields to render</p></li>
<li><p><strong>options</strong> a RenderOptions instance</p></li>
</ul>
</dd>
<dt class="field-even">Returns</dt>
<dd class="field-even"><p>tuple of list of rendered strings and list of unmatched template values</p>
</dd>
<dt class="field-odd">Return type</dt>
<dd class="field-odd"><p>([rendered_strings], [unmatched])</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.score">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">score</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.score" title="Permalink to this definition"></a></dt>
<dd><p>Computed score information for a photo</p>
<dl class="field-list simple">
<dt class="field-odd">Returns</dt>
<dd class="field-odd"><p>ScoreInfo instance</p>
</dd>
</dl>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.screenshot">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">screenshot</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.screenshot" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is an HDR photo, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.search_info">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">search_info</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.search_info" title="Permalink to this definition"></a></dt>
<dd><p>returns SearchInfo object for photo
only valid on Photos 5, on older libraries, returns None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.search_info_normalized">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">search_info_normalized</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.search_info_normalized" title="Permalink to this definition"></a></dt>
<dd><p>returns SearchInfo object for photo that produces normalized results
only valid on Photos 5, on older libraries, returns None</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.selfie">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">selfie</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.selfie" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a selfie (front facing camera), otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.shared">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">shared</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.shared" title="Permalink to this definition"></a></dt>
<dd><p>returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.slow_mo">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">slow_mo</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.slow_mo" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a slow motion video, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.time_lapse">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">time_lapse</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.time_lapse" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a time lapse video, otherwise False</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.title">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">title</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.title" title="Permalink to this definition"></a></dt>
<dd><p>name / title of picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.tzoffset">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">tzoffset</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.tzoffset" title="Permalink to this definition"></a></dt>
<dd><p>timezone offset from UTC in seconds</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.uti">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">uti</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.uti" title="Permalink to this definition"></a></dt>
<dd><p>Returns Uniform Type Identifier (UTI) for the image
for example: public.jpeg or com.apple.quicktime-movie</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.uti_edited">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">uti_edited</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.uti_edited" title="Permalink to this definition"></a></dt>
<dd><p>Returns Uniform Type Identifier (UTI) for the edited image
if the photo has been edited, otherwise None;
for example: public.jpeg</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.uti_original">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">uti_original</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.uti_original" title="Permalink to this definition"></a></dt>
<dd><p>Returns Uniform Type Identifier (UTI) for the original image
for example: public.jpeg or com.apple.quicktime-movie</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.uti_raw">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">uti_raw</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.uti_raw" title="Permalink to this definition"></a></dt>
<dd><p>Returns Uniform Type Identifier (UTI) for the RAW image if there is one
for example: com.canon.cr2-raw-image
Returns None if no associated RAW image</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.uuid">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">uuid</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.uuid" title="Permalink to this definition"></a></dt>
<dd><p>UUID of picture</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.visible">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">visible</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.visible" title="Permalink to this definition"></a></dt>
<dd><p>True if picture is visble</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.width">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">width</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.width" title="Permalink to this definition"></a></dt>
<dd><p>returns width of the current photo version in pixels</p>
</dd></dl>
</dd></dl>
</section>
</section>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.45.10 documentation</title>
<title>Search &#8212; osxphotos 0.47.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />

File diff suppressed because one or more lines are too long

View File

@@ -3,4 +3,6 @@ osxphotos command line interface (CLI)
.. click:: osxphotos.cli:cli
:prog: osxphotos
:nested: full
:nested: full
.. program-output:: python3 -m osxphotos --help

View File

@@ -16,7 +16,7 @@ import sys
import sphinx_rtd_theme
sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information -----------------------------------------------------

View File

@@ -1,7 +1,7 @@
from ._constants import AlbumSortOrder
from ._version import __version__
from .exiftool import ExifTool
from .export_db import ExportDB, ExportDBInMemory, ExportDBNoOp
from .export_db import ExportDB
from .fileutil import FileUtil, FileUtilNoOp
from .momentinfo import MomentInfo
from .personinfo import PersonInfo
@@ -25,8 +25,7 @@ __all__ = [
"CommentInfo",
"ExifTool",
"ExportDB",
"ExportDBInMemory",
"ExportDBNoOp",
"ExportDBTemp",
"ExportOptions",
"ExportResults",
"FileUtil",

View File

@@ -1,6 +1,6 @@
"""Command line interface for osxphotos """
from .cli import cli
from .cli.cli import cli_main
if __name__ == "__main__":
cli() # pylint: disable=no-value-for-parameter
cli_main()

View File

@@ -103,6 +103,8 @@ _TESTED_OS_VERSIONS = [
("11", "6"),
("12", "0"),
("12", "1"),
("12", "2"),
("12", "3"),
]
# Photos 5 has persons who are empty string if unidentified face
@@ -231,10 +233,6 @@ DEFAULT_ORIGINAL_SUFFIX = ""
# Default suffix to add to preview images
DEFAULT_PREVIEW_SUFFIX = "_preview"
# Colors for print CLI messages
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
# Bit masks for --sidecar
SIDECAR_JSON = 0x1
SIDECAR_EXIFTOOL = 0x2
@@ -259,6 +257,7 @@ EXTENDED_ATTRIBUTE_NAMES = [
]
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
# name of export DB
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.45.10"
__version__ = "0.47.2"

56
osxphotos/cli/__init__.py Normal file
View File

@@ -0,0 +1,56 @@
"""cli package for osxphotos"""
from rich.traceback import install as install_traceback
from .about import about
from .albums import albums
from .cli import cli_main
from .common import get_photos_db, load_uuid_from_file, set_debug
from .debug_dump import debug_dump
from .dump import dump
from .export import export
from .exportdb import exportdb
from .grep import grep
from .help import help
from .info import info
from .install_uninstall_run import install, run, uninstall
from .keywords import keywords
from .labels import labels
from .list import _list_libraries, list_libraries
from .persons import persons
from .places import places
from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .tutorial import tutorial
from .uuid import uuid
install_traceback()
__all__ = [
"about",
"albums",
"cli_main",
"debug_dump",
"diff",
"dump",
"export",
"exportdb",
"grep",
"help",
"info",
"install",
"keywords",
"labels",
"list_libraries",
"list_libraries",
"load_uuid_from_file",
"persons",
"places",
"query",
"repl",
"run",
"snap",
"tutorial",
"uuid",
]

66
osxphotos/cli/about.py Normal file
View File

@@ -0,0 +1,66 @@
"""about command for osxphotos CLI"""
import click
from osxphotos._constants import OSXPHOTOS_URL
from osxphotos._version import __version__
@click.command(name="about")
@click.pass_obj
@click.pass_context
def about(ctx, cli_obj):
"""Print information about osxphotos including license."""
license = """
MIT License
Copyright (c) 2019-2021 Rhet Turnbull
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
osxphotos uses the following 3rd party software licensed under the BSD-3-Clause License:
Click (Copyright 2014 Pallets), ptpython (Copyright (c) 2015, Jonathan Slenders)
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be
used to endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
click.echo(f"osxphotos, version {__version__}")
click.echo("")
click.echo(f"Source code available at: {OSXPHOTOS_URL}")
click.echo(license)

42
osxphotos/cli/albums.py Normal file
View File

@@ -0,0 +1,42 @@
"""albums command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
from osxphotos._constants import _PHOTOS_4_VERSION
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def albums(ctx, cli_obj, db, json_, photos_library):
"""Print out albums found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["albums"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
albums = {"albums": photosdb.albums_as_dict}
if photosdb.db_version > _PHOTOS_4_VERSION:
albums["shared albums"] = photosdb.albums_shared_as_dict
if json_ or cli_obj.json:
click.echo(json.dumps(albums, ensure_ascii=False))
else:
click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True))

85
osxphotos/cli/cli.py Normal file
View File

@@ -0,0 +1,85 @@
"""Command line interface for osxphotos """
import click
import osxphotos
from osxphotos._version import __version__
from .about import about
from .albums import albums
from .common import DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN
from .debug_dump import debug_dump
from .dump import dump
from .export import export
from .exportdb import exportdb
from .grep import grep
from .help import help
from .info import info
from .install_uninstall_run import install, run, uninstall
from .keywords import keywords
from .labels import labels
from .list import _list_libraries, list_libraries
from .persons import persons
from .places import places
from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .tutorial import tutorial
from .uuid import uuid
# Click CLI object & context settings
class CLI_Obj:
def __init__(self, db=None, json=False, debug=False, group=None):
if debug:
osxphotos._set_debug(True)
self.db = db
self.json = json
self.group = group
CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
@click.group(context_settings=CTX_SETTINGS)
@DB_OPTION
@JSON_OPTION
@click.option(
"--debug",
required=False,
is_flag=True,
help="Enable debug output",
hidden=OSXPHOTOS_HIDDEN,
)
@click.version_option(__version__, "--version", "-v")
@click.pass_context
def cli_main(ctx, db, json_, debug):
ctx.obj = CLI_Obj(db=db, json=json_, group=cli_main)
# install CLI commands
for command in [
about,
albums,
debug_dump,
diff,
dump,
export,
exportdb,
grep,
help,
info,
install,
keywords,
labels,
list_libraries,
persons,
places,
query,
repl,
snap,
tutorial,
uninstall,
uuid,
]:
cli_main.add_command(command)

538
osxphotos/cli/common.py Normal file
View File

@@ -0,0 +1,538 @@
"""Globals and constants use by the CLI commands"""
import datetime
import os
import pathlib
from typing import Callable
import click
import osxphotos
from osxphotos._version import __version__
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
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
# used by snap and diff commands
OSXPHOTOS_SNAPSHOT_DIR = "/private/tmp/osxphotos_snapshots"
# where to write the crash report if osxphotos crashes
OSXPHOTOS_CRASH_LOG = os.getcwd() + "/osxphotos_crash.log"
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
def set_debug(debug: bool):
"""set debug flag"""
global DEBUG
DEBUG = debug
def is_debug():
"""return debug flag"""
return DEBUG
def noop(*args, **kwargs):
"""no-op function"""
pass
def verbose_print(
verbose: bool = True, timestamp: bool = False, rich=False
) -> 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
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):
"""Return path to photos db, select first non-None db_options
If no db_options are non-None, try to find library to use in
the following order:
- last library opened
- system library
- ~/Pictures/Photos Library.photoslibrary
- failing above, returns None
"""
if db_options:
for db in db_options:
if db is not None:
return db
# if get here, no valid database paths passed, so try to figure out which to use
db = osxphotos.utils.get_last_library_path()
if db is not None:
click.echo(f"Using last opened Photos library: {db}", err=True)
return db
db = osxphotos.utils.get_system_library_path()
if db is not None:
click.echo(f"Using system Photos library: {db}", err=True)
return db
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
if os.path.isdir(db):
click.echo(f"Using Photos library: {db}", err=True)
return db
else:
return None
DB_OPTION = click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help=(
"Specify Photos database path. "
"Path to Photos library/database can be specified using either --db "
"or directly as PHOTOS_LIBRARY positional argument. "
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
),
type=click.Path(exists=True),
)
DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
JSON_OPTION = click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
def DELETED_OPTIONS(f):
o = click.option
options = [
o(
"--deleted",
is_flag=True,
help="Include photos from the 'Recently Deleted' folder.",
),
o(
"--deleted-only",
is_flag=True,
help="Include only photos from the 'Recently Deleted' folder.",
),
]
for o in options[::-1]:
f = o(f)
return f
def QUERY_OPTIONS(f):
o = click.option
options = [
o(
"--keyword",
metavar="KEYWORD",
default=None,
multiple=True,
help="Search for photos with keyword KEYWORD. "
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
),
o(
"--person",
metavar="PERSON",
default=None,
multiple=True,
help="Search for photos with person PERSON. "
'If more than one person, treated as "OR", e.g. find photos matching any person',
),
o(
"--album",
metavar="ALBUM",
default=None,
multiple=True,
help="Search for photos in album ALBUM. "
'If more than one album, treated as "OR", e.g. find photos matching any album',
),
o(
"--folder",
metavar="FOLDER",
default=None,
multiple=True,
help="Search for photos in an album in folder FOLDER. "
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
"Only searches top level folders (e.g. does not look at subfolders)",
),
o(
"--name",
metavar="FILENAME",
default=None,
multiple=True,
help="Search for photos with filename matching FILENAME. "
'If more than one --name options is specified, they are treated as "OR", '
"e.g. find photos matching any FILENAME. ",
),
o(
"--uuid",
metavar="UUID",
default=None,
multiple=True,
help="Search for photos with UUID(s). "
"May be repeated to include multiple UUIDs.",
),
o(
"--uuid-from-file",
metavar="FILE",
default=None,
multiple=False,
help="Search for photos with UUID(s) loaded from FILE. "
"Format is a single UUID per line. Lines preceded with # are ignored.",
type=click.Path(exists=True),
),
o(
"--title",
metavar="TITLE",
default=None,
multiple=True,
help="Search for TITLE in title of photo.",
),
o("--no-title", is_flag=True, help="Search for photos with no title."),
o(
"--description",
metavar="DESC",
default=None,
multiple=True,
help="Search for DESC in description of photo.",
),
o(
"--no-description",
is_flag=True,
help="Search for photos with no description.",
),
o(
"--place",
metavar="PLACE",
default=None,
multiple=True,
help="Search for PLACE in photo's reverse geolocation info",
),
o(
"--no-place",
is_flag=True,
help="Search for photos with no associated place name info (no reverse geolocation info)",
),
o(
"--location",
is_flag=True,
help="Search for photos with associated location info (e.g. GPS coordinates)",
),
o(
"--no-location",
is_flag=True,
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
),
o(
"--label",
metavar="LABEL",
multiple=True,
help="Search for photos with image classification label LABEL (Photos 5 only). "
'If more than one label, treated as "OR", e.g. find photos matching any label',
),
o(
"--uti",
metavar="UTI",
default=None,
multiple=False,
help="Search for photos whose uniform type identifier (UTI) matches UTI",
),
o(
"-i",
"--ignore-case",
is_flag=True,
help="Case insensitive search for title, description, place, keyword, person, or album.",
),
o("--edited", is_flag=True, help="Search for photos that have been edited."),
o(
"--external-edit",
is_flag=True,
help="Search for photos edited in external editor.",
),
o("--favorite", is_flag=True, help="Search for photos marked favorite."),
o(
"--not-favorite",
is_flag=True,
help="Search for photos not marked favorite.",
),
o("--hidden", is_flag=True, help="Search for photos marked hidden."),
o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."),
o(
"--shared",
is_flag=True,
help="Search for photos in shared iCloud album (Photos 5 only).",
),
o(
"--not-shared",
is_flag=True,
help="Search for photos not in shared iCloud album (Photos 5 only).",
),
o(
"--burst",
is_flag=True,
help="Search for photos that were taken in a burst.",
),
o(
"--not-burst",
is_flag=True,
help="Search for photos that are not part of a burst.",
),
o("--live", is_flag=True, help="Search for Apple live photos"),
o(
"--not-live",
is_flag=True,
help="Search for photos that are not Apple live photos.",
),
o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."),
o(
"--not-portrait",
is_flag=True,
help="Search for photos that are not Apple portrait mode photos.",
),
o("--screenshot", is_flag=True, help="Search for screenshot photos."),
o(
"--not-screenshot",
is_flag=True,
help="Search for photos that are not screenshot photos.",
),
o("--slow-mo", is_flag=True, help="Search for slow motion videos."),
o(
"--not-slow-mo",
is_flag=True,
help="Search for photos that are not slow motion videos.",
),
o("--time-lapse", is_flag=True, help="Search for time lapse videos."),
o(
"--not-time-lapse",
is_flag=True,
help="Search for photos that are not time lapse videos.",
),
o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."),
o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."),
o(
"--selfie",
is_flag=True,
help="Search for selfies (photos taken with front-facing cameras).",
),
o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."),
o("--panorama", is_flag=True, help="Search for panorama photos."),
o(
"--not-panorama",
is_flag=True,
help="Search for photos that are not panoramas.",
),
o(
"--has-raw",
is_flag=True,
help="Search for photos with both a jpeg and raw version",
),
o(
"--only-movies",
is_flag=True,
help="Search only for movies (default searches both images and movies).",
),
o(
"--only-photos",
is_flag=True,
help="Search only for photos/images (default searches both images and movies).",
),
o(
"--from-date",
help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(),
),
o(
"--to-date",
help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(),
),
o(
"--from-time",
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
type=TimeISO8601(),
),
o(
"--to-time",
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
type=TimeISO8601(),
),
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
o(
"--is-reference",
is_flag=True,
help="Search for photos that were imported as referenced files (not copied into Photos library).",
),
o(
"--in-album",
is_flag=True,
help="Search for photos that are in one or more albums.",
),
o(
"--not-in-album",
is_flag=True,
help="Search for photos that are not in any albums.",
),
o(
"--duplicate",
is_flag=True,
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
"times or duplicated within Photos.",
),
o(
"--min-size",
metavar="SIZE",
type=BitMathSize(),
help="Search for photos with size >= SIZE bytes. "
"The size evaluated is the photo's original size (when imported to Photos). "
"Size may be specified as integer bytes or using SI or NIST units. "
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
),
o(
"--max-size",
metavar="SIZE",
type=BitMathSize(),
help="Search for photos with size <= SIZE bytes. "
"The size evaluated is the photo's original size (when imported to Photos). "
"Size may be specified as integer bytes or using SI or NIST units. "
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
),
o(
"--regex",
metavar="REGEX TEMPLATE",
nargs=2,
multiple=True,
help="Search for photos where TEMPLATE matches regular expression REGEX. "
"For example, to find photos in an album that begins with 'Beach': '--regex \"^Beach\" \"{album}\"'. "
"You may specify more than one regular expression match by repeating '--regex' with different arguments.",
),
o(
"--selected",
is_flag=True,
help="Filter for photos that are currently selected in Photos.",
),
o(
"--exif",
metavar="EXIF_TAG VALUE",
nargs=2,
multiple=True,
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
"To use --exif, exiftool must be installed and in the path.",
),
o(
"--query-eval",
metavar="CRITERIA",
multiple=True,
help="Evaluate CRITERIA to filter photos. "
"CRITERIA will be evaluated in context of the following python list comprehension: "
"`photos = [photo for photo in photos if CRITERIA]` "
"where photo represents a PhotoInfo object. "
"For example: `--query-eval photo.favorite` returns all photos that have been "
"favorited and is equivalent to --favorite. "
"You may specify more than one CRITERIA by using --query-eval multiple times. "
"CRITERIA must be a valid python expression. "
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
),
o(
"--query-function",
metavar="filename.py::function",
multiple=True,
type=FunctionCall(),
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
+ "file you've created and function is the name of the function in the python file you want to call. "
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
+ "You may use more than one function by repeating the --query-function option with a different value. "
+ "Your query function will be called after all other query options have been evaluated. "
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
),
]
for o in options[::-1]:
f = o(f)
return f
def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored.
Whitespace is stripped.
Arguments:
filename: file name of the file containing UUIDs
Returns:
list of UUIDs or empty list of no UUIDs in file
Raises:
FileNotFoundError if file does not exist
"""
if not pathlib.Path(filename).is_file():
raise FileNotFoundError(f"Could not find file {filename}")
uuid = []
with open(filename, "r") as uuid_file:
for line in uuid_file:
line = line.strip()
if len(line) and line[0] != "#":
uuid.append(line)
return uuid

103
osxphotos/cli/debug_dump.py Normal file
View File

@@ -0,0 +1,103 @@
"""debug-dump command for osxphotos CLI"""
import pprint
import time
import click
from rich import print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .common import (
DB_ARGUMENT,
DB_OPTION,
JSON_OPTION,
OSXPHOTOS_HIDDEN,
get_photos_db,
verbose_print,
)
from .list import _list_libraries
@click.command(hidden=OSXPHOTOS_HIDDEN)
@DB_OPTION
@DB_ARGUMENT
@click.option(
"--dump",
metavar="ATTR",
help="Name of PhotosDB attribute to print; "
+ "can also use albums, persons, keywords, photos to dump related attributes.",
multiple=True,
)
@click.option(
"--uuid",
metavar="UUID",
help="Use with '--dump photos' to dump only certain UUIDs. "
"May be repeated to include multiple UUIDs.",
multiple=True,
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@click.pass_obj
@click.pass_context
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
"""Print out debug info"""
verbose_ = verbose_print(verbose, rich=True)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
start_t = time.perf_counter()
print(f"Opening database: {db}")
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
stop_t = time.perf_counter()
print(f"Done; took {(stop_t-start_t):.2f} seconds")
for attr in dump:
if attr == "albums":
print("_dbalbums_album:")
pprint.pprint(photosdb._dbalbums_album)
print("_dbalbums_uuid:")
pprint.pprint(photosdb._dbalbums_uuid)
print("_dbalbum_details:")
pprint.pprint(photosdb._dbalbum_details)
print("_dbalbum_folders:")
pprint.pprint(photosdb._dbalbum_folders)
print("_dbfolder_details:")
pprint.pprint(photosdb._dbfolder_details)
elif attr == "keywords":
print("_dbkeywords_keyword:")
pprint.pprint(photosdb._dbkeywords_keyword)
print("_dbkeywords_uuid:")
pprint.pprint(photosdb._dbkeywords_uuid)
elif attr == "persons":
print("_dbfaces_uuid:")
pprint.pprint(photosdb._dbfaces_uuid)
print("_dbfaces_pk:")
pprint.pprint(photosdb._dbfaces_pk)
print("_dbpersons_pk:")
pprint.pprint(photosdb._dbpersons_pk)
print("_dbpersons_fullname:")
pprint.pprint(photosdb._dbpersons_fullname)
elif attr == "photos":
if uuid:
for uuid_ in uuid:
print(f"_dbphotos['{uuid_}']:")
try:
pprint.pprint(photosdb._dbphotos[uuid_])
except KeyError:
print(f"Did not find uuid {uuid_} in _dbphotos")
else:
print("_dbphotos:")
pprint.pprint(photosdb._dbphotos)
else:
try:
val = getattr(photosdb, attr)
print(f"{attr}:")
pprint.pprint(val)
except Exception:
print(f"Did not find attribute {attr} in PhotosDB")

44
osxphotos/cli/dump.py Normal file
View File

@@ -0,0 +1,44 @@
"""dump command for osxphotos CLI """
import click
import osxphotos
from osxphotos.queryoptions import QueryOptions
from .common import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, JSON_OPTION, get_photos_db
from .list import _list_libraries
from .print_photo_info import print_photo_info
@click.command()
@DB_OPTION
@JSON_OPTION
@DELETED_OPTIONS
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library):
"""Print list of all photos & associated info from the Photos library."""
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
# check exclusive options
if deleted and deleted_only:
click.echo("Incompatible dump options", err=True)
click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True)
return
photosdb = osxphotos.PhotosDB(dbfile=db)
if deleted or deleted_only:
photos = photosdb.photos(movies=True, intrash=True)
else:
photos = []
if not deleted_only:
photos += photosdb.photos(movies=True)
print_photo_info(photos, json_ or cli_obj.json)

File diff suppressed because it is too large Load Diff

251
osxphotos/cli/exportdb.py Normal file
View File

@@ -0,0 +1,251 @@
"""exportdb command for osxphotos CLI"""
import pathlib
import sys
import click
from rich import print
from osxphotos._constants import OSXPHOTOS_EXPORT_DB
from osxphotos._version import __version__
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
from osxphotos.export_db_utils import (
export_db_check_signatures,
export_db_get_last_run,
export_db_get_version,
export_db_save_config_to_file,
export_db_touch_files,
export_db_update_signatures,
export_db_vacuum,
)
from .common import OSXPHOTOS_HIDDEN, verbose_print
@click.command(name="exportdb", hidden=OSXPHOTOS_HIDDEN)
@click.option("--version", is_flag=True, help="Print export database version and exit.")
@click.option("--vacuum", is_flag=True, help="Run VACUUM to defragment the database.")
@click.option(
"--check-signatures",
is_flag=True,
help="Check signatures for all exported photos in the database to find signatures that don't match.",
)
@click.option(
"--update-signatures",
is_flag=True,
help="Update signatures for all exported photos in the database to match on-disk signatures.",
)
@click.option(
"--touch-file",
is_flag=True,
help="Touch files on disk to match created date in Photos library and update export database signatures",
)
@click.option(
"--last-run",
is_flag=True,
help="Show last run osxphotos commands used with this database.",
)
@click.option(
"--save-config",
metavar="CONFIG_FILE",
help="Save last run configuration to TOML file for use by --load-config.",
)
@click.option(
"--info",
metavar="FILE_PATH",
nargs=1,
help="Print information about FILE_PATH contained in the database.",
)
@click.option(
"--migrate",
is_flag=True,
help="Migrate (if needed) export database to current version.",
)
@click.option(
"--sql",
metavar="SQL_STATEMENT",
help="Execute SQL_STATEMENT against export database and print results.",
)
@click.option(
"--export-dir",
help="Optional path to export directory (if not parent of export database).",
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
@click.option(
"--dry-run",
is_flag=True,
help="Run in dry-run mode (don't actually update files), e.g. for use with --update-signatures.",
)
@click.argument("export_db", metavar="EXPORT_DATABASE", type=click.Path(exists=True))
def exportdb(
version,
vacuum,
check_signatures,
update_signatures,
touch_file,
last_run,
save_config,
info,
migrate,
sql,
export_dir,
verbose,
dry_run,
export_db,
):
"""Utilities for working with the osxphotos export database"""
verbose_ = verbose_print(verbose, rich=True)
export_db = pathlib.Path(export_db)
if export_db.is_dir():
# assume it's the export folder
export_db = export_db / OSXPHOTOS_EXPORT_DB
if not export_db.is_file():
print(
f"[red]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/red]"
)
sys.exit(1)
export_dir = export_dir or export_db.parent
sub_commands = [
version,
check_signatures,
update_signatures,
touch_file,
last_run,
bool(save_config),
bool(info),
migrate,
bool(sql),
]
if sum(sub_commands) > 1:
print("[red]Only a single sub-command may be specified at a time[/red]")
sys.exit(1)
if version:
try:
osxphotos_ver, export_db_ver = export_db_get_version(export_db)
except Exception as e:
print(f"[red]Error: could not read version from {export_db}: {e}[/red]")
sys.exit(1)
else:
print(
f"osxphotos version: {osxphotos_ver}, export database version: {export_db_ver}"
)
sys.exit(0)
if vacuum:
try:
start_size = pathlib.Path(export_db).stat().st_size
export_db_vacuum(export_db)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Vacuumed {export_db}! {start_size} bytes -> {pathlib.Path(export_db).stat().st_size} bytes"
)
sys.exit(0)
if update_signatures:
try:
updated, skipped = export_db_update_signatures(
export_db, export_dir, verbose_, dry_run
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"Done. Updated {updated} files, skipped {skipped} files.")
sys.exit(0)
if last_run:
try:
last_run_info = export_db_get_last_run(export_db)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"last run at {last_run_info[0]}:")
print(f"osxphotos {last_run_info[1]}")
sys.exit(0)
if save_config:
try:
export_db_save_config_to_file(export_db, save_config)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(f"Saved configuration to {save_config}")
sys.exit(0)
if check_signatures:
try:
matched, notmatched, skipped = export_db_check_signatures(
export_db, export_dir, verbose_=verbose_
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Done. Found {matched} matching signatures and {notmatched} signatures that don't match. Skipped {skipped} missing files."
)
sys.exit(0)
if touch_file:
try:
touched, not_touched, skipped = export_db_touch_files(
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
print(
f"Done. Touched {touched} files, skipped {not_touched} up to date files, skipped {skipped} missing files."
)
sys.exit(0)
if info:
exportdb = ExportDB(export_db, export_dir)
try:
info_rec = exportdb.get_file_record(info)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
if info_rec:
print(info_rec.asdict())
else:
print(f"[red]File '{info}' not found in export database[/red]")
sys.exit(0)
if migrate:
exportdb = ExportDB(export_db, export_dir)
if upgraded := exportdb.was_upgraded:
print(
f"Migrated export database {export_db} from version {upgraded[0]} to {upgraded[1]}"
)
else:
print(
f"Export database {export_db} is already at latest version {OSXPHOTOS_EXPORTDB_VERSION}"
)
sys.exit(0)
if sql:
exportdb = ExportDB(export_db, export_dir)
try:
c = exportdb._conn.cursor()
results = c.execute(sql)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
for row in results:
print(row)
sys.exit(0)

57
osxphotos/cli/grep.py Normal file
View File

@@ -0,0 +1,57 @@
"""grep command for osxphotos CLI """
import pathlib
import click
from rich import print
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
from osxphotos.sqlgrep import sqlgrep
from .common import DB_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
@click.command(name="grep", hidden=OSXPHOTOS_HIDDEN)
@DB_OPTION
@click.pass_obj
@click.pass_context
@click.option(
"--ignore-case",
"-i",
required=False,
is_flag=True,
default=False,
help="Ignore case when searching (default is case-sensitive).",
)
@click.option(
"--print-filename",
"-p",
required=False,
is_flag=True,
default=False,
help="Print name of database file when printing results.",
)
@click.argument("pattern", metavar="PATTERN", required=True)
def grep(ctx, cli_obj, db, ignore_case, print_filename, pattern):
"""Search for PATTERN in the Photos sqlite database file"""
db = db or get_photos_db()
db = pathlib.Path(db)
if db.is_file():
# if passed the actual database, really want the parent of the database directory
db = db.parent.parent
photos_ver = get_photos_library_version(str(db))
if photos_ver < 5:
db_file = db / "database" / "photos.db"
else:
db_file = db / "database" / "Photos.sqlite"
if not db_file.is_file():
click.secho(f"Could not find database file {db_file}", fg="red")
ctx.exit(2)
db_file = str(db_file)
for table, column, row_id, value in sqlgrep(
db_file, pattern, ignore_case, print_filename, rich_markup=True
):
print(", ".join([table, column, row_id, value]))

View File

@@ -1,7 +1,6 @@
"""Help text helper class for osxphotos CLI """
import io
import pathlib
import re
import click
@@ -9,13 +8,13 @@ import osxmetadata
from rich.console import Console
from rich.markdown import Markdown
from ._constants import (
from osxphotos._constants import (
EXTENDED_ATTRIBUTE_NAMES,
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
OSXPHOTOS_EXPORT_DB,
POST_COMMAND_CATEGORIES,
)
from .phototemplate import (
from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
TEMPLATE_SUBSTITUTIONS_PATHLIB,
@@ -25,15 +24,37 @@ from .phototemplate import (
__all__ = [
"ExportCommand",
"template_help",
"tutorial_help",
"rich_text",
"strip_md_header_and_links",
"strip_md_links",
"strip_html_comments",
"get_tutorial_text",
"help",
"get_help_msg",
]
def get_help_msg(command):
"""get help message for a Click command"""
with click.Context(command) as ctx:
return command.get_help(ctx)
@click.command()
@click.argument("topic", default=None, required=False, nargs=1)
@click.pass_context
def help(ctx, topic, **kw):
"""Print help; for help on commands: help <command>."""
if topic is None:
click.echo(ctx.parent.get_help())
return
elif topic in ctx.obj.group.commands:
ctx.info_name = topic
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
else:
click.echo(f"Invalid command: {topic}", err=True)
click.echo(ctx.parent.get_help())
# TODO: The following help text could probably be done as mako template
class ExportCommand(click.Command):
"""Custom click.Command that overrides get_help() to show additional help info for export"""
@@ -282,19 +303,6 @@ def template_help(width=78):
return help_str
def tutorial_help(width=78):
"""Return formatted string for tutorial"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
help_md = get_tutorial_text()
help_md = strip_html_comments(help_md)
help_md = strip_md_links(help_md)
console.print(Markdown(help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def rich_text(text, width=78):
"""Return rich formatted text"""
sio = io.StringIO()
@@ -348,12 +356,3 @@ def strip_md_links(md):
def strip_html_comments(text):
"""Strip html comments from text (which doesn't need to be valid HTML)"""
return re.sub(r"<!--(.|\s|\n)*?-->", "", text)
def get_tutorial_text():
"""Load tutorial text from file"""
# TODO: would be better to use importlib.abc.ResourceReader but I can't find a single example of how to do this
help_file = pathlib.Path(__file__).parent / "tutorial.md"
with open(help_file, "r") as fd:
md = fd.read()
return md

72
osxphotos/cli/info.py Normal file
View File

@@ -0,0 +1,72 @@
"""info command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def info(ctx, cli_obj, db, json_, photos_library):
"""Print out descriptive info of the Photos library database."""
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["info"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
info = {"database_path": photosdb.db_path, "database_version": photosdb.db_version}
photos = photosdb.photos(movies=False)
not_shared_photos = [p for p in photos if not p.shared]
info["photo_count"] = len(not_shared_photos)
hidden = [p for p in photos if p.hidden]
info["hidden_photo_count"] = len(hidden)
movies = photosdb.photos(images=False, movies=True)
not_shared_movies = [p for p in movies if not p.shared]
info["movie_count"] = len(not_shared_movies)
if photosdb.db_version > _PHOTOS_4_VERSION:
shared_photos = [p for p in photos if p.shared]
info["shared_photo_count"] = len(shared_photos)
shared_movies = [p for p in movies if p.shared]
info["shared_movie_count"] = len(shared_movies)
keywords = photosdb.keywords_as_dict
info["keywords_count"] = len(keywords)
info["keywords"] = keywords
albums = photosdb.albums_as_dict
info["albums_count"] = len(albums)
info["albums"] = albums
if photosdb.db_version > _PHOTOS_4_VERSION:
albums_shared = photosdb.albums_shared_as_dict
info["shared_albums_count"] = len(albums_shared)
info["shared_albums"] = albums_shared
persons = photosdb.persons_as_dict
info["persons_count"] = len(persons)
info["persons"] = persons
if cli_obj.json or json_:
click.echo(json.dumps(info, ensure_ascii=False))
else:
click.echo(yaml.dump(info, sort_keys=False, allow_unicode=True))

View File

@@ -0,0 +1,37 @@
"""install/uninstall/run commands for osxphotos CLI"""
import sys
from runpy import run_module, run_path
import click
@click.command()
@click.argument("packages", nargs=-1, required=True)
@click.option(
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
)
def install(packages, upgrade):
"""Install Python packages into the same environment as osxphotos"""
args = ["pip", "install"]
if upgrade:
args += ["--upgrade"]
args += list(packages)
sys.argv = args
run_module("pip", run_name="__main__")
@click.command()
@click.argument("packages", nargs=-1, required=True)
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
def uninstall(packages, yes):
"""Uninstall Python packages from the osxphotos environment"""
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
run_module("pip", run_name="__main__")
@click.command(name="run")
@click.argument("python_file", nargs=1, type=click.Path(exists=True))
def run(python_file):
"""Run a python file using same environment as osxphotos"""
run_path(python_file, run_name="__main__")

37
osxphotos/cli/keywords.py Normal file
View File

@@ -0,0 +1,37 @@
"""keywords command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def keywords(ctx, cli_obj, db, json_, photos_library):
"""Print out keywords found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["keywords"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
keywords = {"keywords": photosdb.keywords_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(keywords, ensure_ascii=False))
else:
click.echo(yaml.dump(keywords, sort_keys=False, allow_unicode=True))

37
osxphotos/cli/labels.py Normal file
View File

@@ -0,0 +1,37 @@
"""labels command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def labels(ctx, cli_obj, db, json_, photos_library):
"""Print out image classification labels found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["labels"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
labels = {"labels": photosdb.labels_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(labels, ensure_ascii=False))
else:
click.echo(yaml.dump(labels, sort_keys=False, allow_unicode=True))

57
osxphotos/cli/list.py Normal file
View File

@@ -0,0 +1,57 @@
"""list command for osxphotos CLI"""
import json
import click
import osxphotos
from .common import JSON_OPTION
@click.command(name="list")
@JSON_OPTION
@click.pass_obj
@click.pass_context
def list_libraries(ctx, cli_obj, json_):
"""Print list of Photos libraries found on the system."""
# implemented in _list_libraries so it can be called by other CLI functions
# without errors due to passing ctx and cli_obj
_list_libraries(json_=json_ or cli_obj.json, error=False)
def _list_libraries(json_=False, error=True):
"""Print list of Photos libraries found on the system.
If json_ == True, print output as JSON (default = False)"""
photo_libs = osxphotos.utils.list_photo_libraries()
sys_lib = osxphotos.utils.get_system_library_path()
last_lib = osxphotos.utils.get_last_library_path()
if json_:
libs = {
"photo_libraries": photo_libs,
"system_library": sys_lib,
"last_library": last_lib,
}
click.echo(json.dumps(libs, ensure_ascii=False))
else:
last_lib_flag = sys_lib_flag = False
for lib in photo_libs:
if lib == sys_lib:
click.echo(f"(*)\t{lib}", err=error)
sys_lib_flag = True
elif lib == last_lib:
click.echo(f"(#)\t{lib}", err=error)
last_lib_flag = True
else:
click.echo(f"\t{lib}", err=error)
if sys_lib_flag or last_lib_flag:
click.echo("\n", err=error)
if sys_lib_flag:
click.echo("(*)\tSystem Photos Library", err=error)
if last_lib_flag:
click.echo("(#)\tLast opened Photos Library", err=error)

View File

@@ -0,0 +1,108 @@
"""Click parameter types for osxphotos CLI"""
import datetime
import pathlib
import bitmath
import click
from osxphotos.export_db_utils import export_db_get_version
from osxphotos.utils import expand_and_validate_filepath, load_function
__all__ = [
"BitMathSize",
"DateTimeISO8601",
"ExportDBType",
"FunctionCall",
"TimeISO8601",
]
class DateTimeISO8601(click.ParamType):
name = "DATETIME"
def convert(self, value, param, ctx):
try:
return datetime.datetime.fromisoformat(value)
except Exception:
self.fail(
f"Invalid datetime format {value}. "
"Valid format: YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]"
)
class BitMathSize(click.ParamType):
name = "BITMATH"
def convert(self, value, param, ctx):
try:
value = bitmath.parse_string(value)
except ValueError:
# no units specified
try:
value = int(value)
value = bitmath.Byte(value)
except ValueError as e:
self.fail(
f"{value} must be specified as bytes or using SI/NIST units. "
+ "For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'."
)
return value
class TimeISO8601(click.ParamType):
name = "TIME"
def convert(self, value, param, ctx):
try:
return datetime.time.fromisoformat(value).replace(tzinfo=None)
except Exception:
self.fail(
f"Invalid time format {value}. "
"Valid format: HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] "
"however, note that timezone will be ignored."
)
class FunctionCall(click.ParamType):
name = "FUNCTION"
def convert(self, value, param, ctx):
if "::" not in value:
self.fail(
f"Could not parse function name from '{value}'. "
"Valid format filename.py::function"
)
filename, funcname = value.split("::")
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
self.fail(f"'{filename}' does not appear to be a file")
try:
function = load_function(filename_validated, funcname)
except Exception as e:
self.fail(f"Could not load function {funcname} from {filename_validated}")
return (function, value)
class ExportDBType(click.ParamType):
name = "EXPORTDB"
def convert(self, value, param, ctx):
try:
export_db_name = pathlib.Path(value)
if export_db_name.is_dir():
raise click.BadParameter(f"{value} is a directory")
if export_db_name.is_file():
# verify it's actually an osxphotos export_db
# export_db_get_version will raise an error if it's not valid
osxphotos_ver, export_db_ver = export_db_get_version(value)
return value
except Exception:
self.fail(f"{value} exists but is not a valid osxphotos export database. ")

36
osxphotos/cli/persons.py Normal file
View File

@@ -0,0 +1,36 @@
"""persons command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def persons(ctx, cli_obj, db, json_, photos_library):
"""Print out persons (faces) found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["persons"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
persons = {"persons": photosdb.persons_as_dict}
if json_ or cli_obj.json:
click.echo(json.dumps(persons, ensure_ascii=False))
else:
click.echo(yaml.dump(persons, sort_keys=False, allow_unicode=True))

62
osxphotos/cli/places.py Normal file
View File

@@ -0,0 +1,62 @@
"""places command for osxphotos CLI"""
import json
import click
import yaml
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
from .list import _list_libraries
@click.command()
@DB_OPTION
@JSON_OPTION
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def places(ctx, cli_obj, db, json_, photos_library):
"""Print out places found in the Photos library."""
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["places"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
place_names = {}
for photo in photosdb.photos(movies=True):
if photo.place:
try:
place_names[photo.place.name] += 1
except Exception:
place_names[photo.place.name] = 1
else:
try:
place_names[_UNKNOWN_PLACE] += 1
except Exception:
place_names[_UNKNOWN_PLACE] = 1
# sort by place count
places = {
"places": {
name: place_names[name]
for name in sorted(
place_names.keys(), key=lambda key: place_names[key], reverse=True
)
}
}
# below needed for to make CliRunner work for testing
cli_json = cli_obj.json if cli_obj is not None else None
if json_ or cli_json:
click.echo(json.dumps(places, ensure_ascii=False))
else:
click.echo(yaml.dump(places, sort_keys=False, allow_unicode=True))

View File

@@ -0,0 +1,112 @@
"""print_photo_info function to print PhotoInfo objects"""
import csv
import sys
from typing import Callable, List
from osxphotos.photoinfo import PhotoInfo
def print_photo_info(
photos: List[PhotoInfo], json: bool = False, print_func: Callable = print
):
dump = []
if json:
dump.extend(p.json() for p in photos)
print_func(f"[{', '.join(dump)}]")
else:
# dump as CSV
csv_writer = csv.writer(
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
)
# add headers
dump.append(
[
"uuid",
"filename",
"original_filename",
"date",
"description",
"title",
"keywords",
"albums",
"persons",
"path",
"ismissing",
"hasadjustments",
"external_edit",
"favorite",
"hidden",
"shared",
"latitude",
"longitude",
"path_edited",
"isphoto",
"ismovie",
"uti",
"burst",
"live_photo",
"path_live_photo",
"iscloudasset",
"incloud",
"date_modified",
"portrait",
"screenshot",
"slow_mo",
"time_lapse",
"hdr",
"selfie",
"panorama",
"has_raw",
"uti_raw",
"path_raw",
"intrash",
]
)
for p in photos:
date_modified_iso = p.date_modified.isoformat() if p.date_modified else None
dump.append(
[
p.uuid,
p.filename,
p.original_filename,
p.date.isoformat(),
p.description,
p.title,
", ".join(p.keywords),
", ".join(p.albums),
", ".join(p.persons),
p.path,
p.ismissing,
p.hasadjustments,
p.external_edit,
p.favorite,
p.hidden,
p.shared,
p._latitude,
p._longitude,
p.path_edited,
p.isphoto,
p.ismovie,
p.uti,
p.burst,
p.live_photo,
p.path_live_photo,
p.iscloudasset,
p.incloud,
date_modified_iso,
p.portrait,
p.screenshot,
p.slow_mo,
p.time_lapse,
p.hdr,
p.selfie,
p.panorama,
p.has_raw,
p.uti_raw,
p.path_raw,
p.intrash,
]
)
for row in dump:
csv_writer.writerow(row)

358
osxphotos/cli/query.py Normal file
View File

@@ -0,0 +1,358 @@
"""query command for osxphotos CLI"""
import click
import osxphotos
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.queryoptions import QueryOptions
from .common import (
CLI_COLOR_ERROR,
CLI_COLOR_WARNING,
DB_ARGUMENT,
DB_OPTION,
DELETED_OPTIONS,
JSON_OPTION,
OSXPHOTOS_HIDDEN,
QUERY_OPTIONS,
get_photos_db,
load_uuid_from_file,
set_debug,
)
from .list import _list_libraries
from .print_photo_info import print_photo_info
@click.command()
@DB_OPTION
@JSON_OPTION
@QUERY_OPTIONS
@DELETED_OPTIONS
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
@click.option(
"--not-missing",
is_flag=True,
help="Search for photos present on disk (e.g. not missing).",
)
@click.option(
"--cloudasset",
is_flag=True,
help="Search for photos that are part of an iCloud library",
)
@click.option(
"--not-cloudasset",
is_flag=True,
help="Search for photos that are not part of an iCloud library",
)
@click.option(
"--incloud",
is_flag=True,
help="Search for photos that are in iCloud (have been synched)",
)
@click.option(
"--not-incloud",
is_flag=True,
help="Search for photos that are not in iCloud (have not been synched)",
)
@click.option(
"--add-to-album",
metavar="ALBUM",
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
"if it doesn't exist. All photos in the query results will be added to this album. "
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
"This feature is currently experimental. I don't know how well it will work on large query sets.",
)
@click.option(
"--debug", required=False, is_flag=True, default=False, hidden=OSXPHOTOS_HIDDEN
)
@DB_ARGUMENT
@click.pass_obj
@click.pass_context
def query(
ctx,
cli_obj,
db,
photos_library,
keyword,
person,
album,
folder,
name,
uuid,
uuid_from_file,
title,
no_title,
description,
no_description,
ignore_case,
json_,
edited,
external_edit,
favorite,
not_favorite,
hidden,
not_hidden,
missing,
not_missing,
shared,
not_shared,
only_movies,
only_photos,
uti,
burst,
not_burst,
live,
not_live,
cloudasset,
not_cloudasset,
incloud,
not_incloud,
from_date,
to_date,
from_time,
to_time,
portrait,
not_portrait,
screenshot,
not_screenshot,
slow_mo,
not_slow_mo,
time_lapse,
not_time_lapse,
hdr,
not_hdr,
selfie,
not_selfie,
panorama,
not_panorama,
has_raw,
place,
no_place,
location,
no_location,
label,
deleted,
deleted_only,
has_comment,
no_comment,
has_likes,
no_likes,
is_reference,
in_album,
not_in_album,
duplicate,
min_size,
max_size,
regex,
selected,
exif,
query_eval,
query_function,
add_to_album,
debug,
):
"""Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND"
(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
# sanity check input args
nonexclusive = [
keyword,
person,
album,
folder,
name,
uuid,
uuid_from_file,
edited,
external_edit,
uti,
has_raw,
from_date,
to_date,
from_time,
to_time,
label,
is_reference,
query_eval,
query_function,
min_size,
max_size,
regex,
selected,
exif,
duplicate,
]
exclusive = [
(favorite, not_favorite),
(hidden, not_hidden),
(missing, not_missing),
(any(title), no_title),
(any(description), no_description),
(only_photos, only_movies),
(burst, not_burst),
(live, not_live),
(cloudasset, not_cloudasset),
(incloud, not_incloud),
(portrait, not_portrait),
(screenshot, not_screenshot),
(slow_mo, not_slow_mo),
(time_lapse, not_time_lapse),
(hdr, not_hdr),
(selfie, not_selfie),
(panorama, not_panorama),
(any(place), no_place),
(deleted, deleted_only),
(shared, not_shared),
(has_comment, no_comment),
(has_likes, no_likes),
(in_album, not_in_album),
(location, no_location),
]
# print help if no non-exclusive term or a double exclusive term is given
if any(all(bb) for bb in exclusive) or not any(
nonexclusive + [b ^ n for b, n in exclusive]
):
click.echo("Incompatible query options", err=True)
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
return
# actually have something to query
# default searches for everything
photos = True
movies = True
if only_movies:
photos = False
if only_photos:
movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
if uuid_from_file:
uuid_list = list(uuid) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(uuid_from_file))
uuid = tuple(uuid_list)
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None
db = get_photos_db(*photos_library, db, cli_db)
if db is None:
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
click.echo("\n\nLocated the following Photos library databases: ", err=True)
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
query_options = QueryOptions(
keyword=keyword,
person=person,
album=album,
folder=folder,
uuid=uuid,
title=title,
no_title=no_title,
description=description,
no_description=no_description,
ignore_case=ignore_case,
edited=edited,
external_edit=external_edit,
favorite=favorite,
not_favorite=not_favorite,
hidden=hidden,
not_hidden=not_hidden,
missing=missing,
not_missing=not_missing,
shared=shared,
not_shared=not_shared,
photos=photos,
movies=movies,
uti=uti,
burst=burst,
not_burst=not_burst,
live=live,
not_live=not_live,
cloudasset=cloudasset,
not_cloudasset=not_cloudasset,
incloud=incloud,
not_incloud=not_incloud,
from_date=from_date,
to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait,
not_portrait=not_portrait,
screenshot=screenshot,
not_screenshot=not_screenshot,
slow_mo=slow_mo,
not_slow_mo=not_slow_mo,
time_lapse=time_lapse,
not_time_lapse=not_time_lapse,
hdr=hdr,
not_hdr=not_hdr,
selfie=selfie,
not_selfie=not_selfie,
panorama=panorama,
not_panorama=not_panorama,
has_raw=has_raw,
place=place,
no_place=no_place,
location=location,
no_location=no_location,
label=label,
deleted=deleted,
deleted_only=deleted_only,
has_comment=has_comment,
no_comment=no_comment,
has_likes=has_likes,
no_likes=no_likes,
is_reference=is_reference,
in_album=in_album,
not_in_album=not_in_album,
name=name,
min_size=min_size,
max_size=max_size,
query_eval=query_eval,
function=query_function,
regex=regex,
selected=selected,
exif=exif,
duplicate=duplicate,
)
try:
photos = photosdb.query(query_options)
except ValueError as e:
if "Invalid query_eval CRITERIA:" in str(e):
msg = str(e).split(":")[1]
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
)
else:
raise ValueError(e)
# below needed for to make CliRunner work for testing
cli_json = cli_obj.json if cli_obj is not None else None
if add_to_album and photos:
album_query = PhotosAlbum(add_to_album, verbose=None)
photo_len = len(photos)
photo_word = "photos" if photo_len > 1 else "photo"
click.echo(
f"Adding {photo_len} {photo_word} to album '{album_query.name}'. Note: Photos may prompt you to confirm this action.",
err=True,
)
try:
album_query.add_list(photos)
except Exception as e:
click.secho(
f"Error adding photos to album {add_to_album}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
print_photo_info(photos, cli_json or json_, print_func=click.echo)

339
osxphotos/cli/repl.py Normal file
View File

@@ -0,0 +1,339 @@
"""repl command for osxphotos CLI"""
import dataclasses
import os
import os.path
import pathlib
import sys
import time
from typing import List
import click
import photoscript
from rich import pretty, print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION
from osxphotos.photoinfo import PhotoInfo
from osxphotos.photosdb import PhotosDB
from osxphotos.pyrepl import embed_repl
from osxphotos.queryoptions import QueryOptions
from .common import (
DB_ARGUMENT,
DB_OPTION,
DELETED_OPTIONS,
QUERY_OPTIONS,
get_photos_db,
load_uuid_from_file,
)
class IncompatibleQueryOptions(Exception):
pass
@click.command(name="repl")
@DB_OPTION
@click.pass_obj
@click.pass_context
@click.option(
"--emacs",
required=False,
is_flag=True,
default=False,
help="Launch REPL with Emacs keybindings (default is vi bindings)",
)
@click.option(
"--beta",
is_flag=True,
default=False,
hidden=True,
help="Enable beta options.",
)
@QUERY_OPTIONS
@DELETED_OPTIONS
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
@click.option(
"--not-missing",
is_flag=True,
help="Search for photos present on disk (e.g. not missing).",
)
@click.option(
"--cloudasset",
is_flag=True,
help="Search for photos that are part of an iCloud library",
)
@click.option(
"--not-cloudasset",
is_flag=True,
help="Search for photos that are not part of an iCloud library",
)
@click.option(
"--incloud",
is_flag=True,
help="Search for photos that are in iCloud (have been synched)",
)
@click.option(
"--not-incloud",
is_flag=True,
help="Search for photos that are not in iCloud (have not been synched)",
)
def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
import logging
from objexplore import explore
from photoscript import Album, Photo, PhotosLibrary
from rich import inspect as _inspect
from osxphotos import ExifTool, PhotoInfo, PhotosDB
from osxphotos.albuminfo import AlbumInfo
from osxphotos.momentinfo import MomentInfo
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
from osxphotos.placeinfo import PlaceInfo
from osxphotos.queryoptions import QueryOptions
from osxphotos.scoreinfo import ScoreInfo
from osxphotos.searchinfo import SearchInfo
logger = logging.getLogger()
logger.disabled = True
pretty.install()
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
db = db or get_photos_db()
photosdb = _load_photos_db(db)
# enable beta features if requested
if beta:
photosdb._beta = beta
print("Beta mode enabled")
print("Getting photos")
tic = time.perf_counter()
try:
query_options = _query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions:
click.echo("Incompatible query options", err=True)
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
sys.exit(1)
photos = _query_photos(photosdb, query_options)
all_photos = _get_all_photos(photosdb)
toc = time.perf_counter()
tictoc = toc - tic
# shortcut for helper functions
get_photo = photosdb.get_photo
show = _show_photo
spotlight = _spotlight_photo
get_selected = _get_selected(photosdb)
try:
selected = get_selected()
except Exception:
# get_selected sometimes fails
selected = []
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
print("The following classes have been imported from osxphotos:")
print(
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportOptions, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
)
print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
print(
f"- photos: list of PhotoInfo objects for all photos filtered with any query options passed on command line (len={len(photos)})"
)
print(
f"- all_photos: list of PhotoInfo objects for all photos in photosdb, including those in the trash (len={len(all_photos)})"
)
print(
f"- selected: list of PhotoInfo objects for any photos selected in Photos (len={len(selected)})"
)
print(f"\nThe following functions may be helpful:")
print(
f"- get_photo(uuid): return a PhotoInfo object for photo with uuid; e.g. get_photo('B13F4485-94E0-41CD-AF71-913095D62E31')"
)
print(
f"- get_selected(); return list of PhotoInfo objects for photos selected in Photos"
)
print(
f"- show(photo): open a photo object in the default viewer; e.g. show(selected[0])"
)
print(
f"- show(path): open a file at path in the default viewer; e.g. show('/path/to/photo.jpg')"
)
print(f"- spotlight(photo): open a photo and spotlight it in Photos")
# print(
# f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
# )
print(
f"- inspect(object): print information about an object; e.g. inspect(PhotoInfo)"
)
print(
f"- explore(object): interactively explore an object with objexplore; e.g. explore(PhotoInfo)"
)
print(f"- q, quit, quit(), exit, exit(): exit this interactive shell\n")
embed_repl(
globals=globals(),
locals=locals(),
history_filename=str(pathlib.Path.home() / ".osxphotos_repl_history"),
quit_words=["q", "quit", "exit"],
vi_mode=not emacs,
)
def _show_photo(photo: PhotoInfo):
"""open image with default image viewer
Note: This is for debugging only -- it will actually open any filetype which could
be very, very bad.
Args:
photo: PhotoInfo object or a path to a photo on disk
"""
photopath = photo.path if isinstance(photo, osxphotos.PhotoInfo) else photo
if not os.path.isfile(photopath):
return f"'{photopath}' does not appear to be a valid photo path"
os.system(f"open '{photopath}'")
def _load_photos_db(dbpath):
print("Loading database")
tic = time.perf_counter()
photosdb = osxphotos.PhotosDB(dbfile=dbpath, verbose=print)
toc = time.perf_counter()
tictoc = toc - tic
print(f"Done: took {tictoc:0.2f} seconds")
return photosdb
def _get_all_photos(photosdb):
"""get list of all photos in photosdb"""
photos = photosdb.photos(images=True, movies=True)
photos.extend(photosdb.photos(images=True, movies=True, intrash=True))
return photos
def _get_selected(photosdb):
"""get list of PhotoInfo objects for photos selected in Photos"""
def get_selected():
selected = photoscript.PhotosLibrary().selection
if not selected:
return []
return photosdb.photos(uuid=[p.uuid for p in selected])
return get_selected
def _spotlight_photo(photo: PhotoInfo):
photo_ = photoscript.Photo(photo.uuid)
photo_.spotlight()
def _query_options_from_kwargs(**kwargs) -> QueryOptions:
"""Validate query options and create a QueryOptions instance"""
# sanity check input args
nonexclusive = [
"keyword",
"person",
"album",
"folder",
"name",
"uuid",
"uuid_from_file",
"edited",
"external_edit",
"uti",
"has_raw",
"from_date",
"to_date",
"from_time",
"to_time",
"label",
"is_reference",
"query_eval",
"query_function",
"min_size",
"max_size",
"regex",
"selected",
"exif",
"duplicate",
]
exclusive = [
("favorite", "not_favorite"),
("hidden", "not_hidden"),
("missing", "not_missing"),
("only_photos", "only_movies"),
("burst", "not_burst"),
("live", "not_live"),
("cloudasset", "not_cloudasset"),
("incloud", "not_incloud"),
("portrait", "not_portrait"),
("screenshot", "not_screenshot"),
("slow_mo", "not_slow_mo"),
("time_lapse", "not_time_lapse"),
("hdr", "not_hdr"),
("selfie", "not_selfie"),
("panorama", "not_panorama"),
("deleted", "deleted_only"),
("shared", "not_shared"),
("has_comment", "no_comment"),
("has_likes", "no_likes"),
("in_album", "not_in_album"),
("location", "no_location"),
]
# print help if no non-exclusive term or a double exclusive term is given
# TODO: add option to validate requiring at least one query arg
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
[
all([any(kwargs["title"]), kwargs["no_title"]]),
all([any(kwargs["description"]), kwargs["no_description"]]),
all([any(kwargs["place"]), kwargs["no_place"]]),
]
):
raise IncompatibleQueryOptions
# actually have something to query
include_photos = True
include_movies = True # default searches for everything
if kwargs["only_movies"]:
include_photos = False
if kwargs["only_photos"]:
include_movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
uuid = None
if kwargs["uuid_from_file"]:
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
uuid = tuple(uuid_list)
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
query_dict = {field: kwargs.get(field) for field in query_fields}
query_dict["photos"] = include_photos
query_dict["movies"] = include_movies
query_dict["uuid"] = uuid
return QueryOptions(**query_dict)
def _query_photos(photosdb: PhotosDB, query_options: QueryOptions) -> List:
"""Query photos given a QueryOptions instance"""
try:
photos = photosdb.query(query_options)
except ValueError as e:
if "Invalid query_eval CRITERIA:" not in str(e):
raise ValueError(e) from e
msg = str(e).split(":")[1]
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
) from e
return photos

157
osxphotos/cli/snap_diff.py Normal file
View File

@@ -0,0 +1,157 @@
"""snap/diff commands for osxphotos CLI"""
import datetime
import os
import pathlib
import shutil
import subprocess
import click
from rich.console import Console
from rich.syntax import Syntax
import osxphotos
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db, verbose_print
@click.command(name="snap")
@click.pass_obj
@click.pass_context
@DB_OPTION
def snap(ctx, cli_obj, db):
"""Create snapshot of Photos database to use with diff command
Snapshots only the database files, not the entire library. If OSXPHOTOS_SNAPSHOT
environment variable is defined, will use that as snapshot directory, otherwise
uses '/private/tmp/osxphotos_snapshots'
Works only on Photos library versions since Catalina (10.15) or newer.
"""
db = get_photos_db(db, cli_obj.db)
db_path = pathlib.Path(db)
if db_path.is_file():
# assume it's the sqlite file
db_path = db_path.parent.parent
db_path = db_path / "database"
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
if not os.path.isdir(db_folder):
click.echo(f"Creating snapshot folder: '{db_folder}'")
os.mkdir(db_folder)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
destination_path = pathlib.Path(db_folder) / timestamp
# get all the sqlite files including the write ahead log if any
files = db_path.glob("*.sqlite*")
os.makedirs(destination_path)
fu = osxphotos.fileutil.FileUtil()
count = 0
for file in files:
if file.is_file():
fu.copy(file, destination_path)
count += 1
print(f"Copied {count} files from {db_path} to {destination_path}")
@click.command(name="diff")
@click.pass_obj
@click.pass_context
@DB_OPTION
@click.option(
"--raw-output",
"-r",
is_flag=True,
default=False,
help="Print raw output (don't use syntax highlighting).",
)
@click.option(
"--style",
"-s",
metavar="STYLE",
nargs=1,
default="monokai",
help="Specify style/theme for syntax highlighting. "
"Theme may be any valid pygments style (https://pygments.org/styles/). "
"Default is 'monokai'.",
)
@click.argument("db2", nargs=-1, type=click.Path(exists=True))
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
"""Compare two Photos databases and print out differences
To use the diff command, you'll need to install sqldiff via homebrew:
- Install homebrew (https://brew.sh/) if not already installed
- Install sqldiff: `brew install sqldiff`
When run with no arguments, compares the current Photos library to the
most recent snapshot in the the OSXPHOTOS_SNAPSHOT directory.
If run with the --db option, compares the library specified by --db to the
most recent snapshot in the the OSXPHOTOS_SNAPSHOT directory.
If run with just the DB2 argument, compares the current Photos library to
the database specified by the DB2 argument.
If run with both the --db option and the DB2 argument, compares the
library specified by --db to the database specified by DB2
See also `osxphotos snap`
If the OSXPHOTOS_SNAPSHOT environment variable is not set, will use
'/private/tmp/osxphotos_snapshots'
Works only on Photos library versions since Catalina (10.15) or newer.
"""
verbose_ = verbose_print(verbose, rich=True)
sqldiff = shutil.which("sqldiff")
if not sqldiff:
click.echo(
"sqldiff not found; install via homebrew (https://brew.sh/): `brew install sqldiff`"
)
ctx.exit(2)
verbose_(f"sqldiff found at '{sqldiff}'")
db = get_photos_db(db, cli_obj.db)
db_path = pathlib.Path(db)
if db_path.is_file():
# assume it's the sqlite file
db_path = db_path.parent.parent
db_path = db_path / "database"
db_1 = db_path / "photos.sqlite"
if db2:
db_2 = pathlib.Path(db2[0])
else:
# get most recent snapshot
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
verbose_(f"Using snapshot folder: '{db_folder}'")
folders = sorted([f for f in pathlib.Path(db_folder).glob("*") if f.is_dir()])
folder_2 = folders[-1]
db_2 = folder_2 / "Photos.sqlite"
if not db_1.exists():
print(f"database file {db_1} missing")
if not db_2.exists():
print(f"database file {db_2} missing")
verbose_(f"Comparing databases {db_1} and {db_2}")
diff_proc = subprocess.Popen([sqldiff, db_2, db_1], stdout=subprocess.PIPE)
console = Console()
for line in iter(diff_proc.stdout.readline, b""):
line = line.decode("UTF-8").rstrip()
if raw_output:
print(line)
else:
syntax = Syntax(
line, "sql", theme=style, line_numbers=False, code_width=1000
)
console.print(syntax)

46
osxphotos/cli/tutorial.py Normal file
View File

@@ -0,0 +1,46 @@
"""tutorial command for osxphotos CLI"""
import io
import pathlib
import click
from rich.console import Console
from rich.markdown import Markdown
from .help import strip_html_comments, strip_md_links
@click.command(name="tutorial")
@click.argument(
"WIDTH",
nargs=-1,
type=click.INT,
)
@click.pass_obj
@click.pass_context
def tutorial(ctx, cli_obj, width):
"""Display osxphotos tutorial."""
width = width[0] if width else 100
click.echo_via_pager(tutorial_help(width=width))
def tutorial_help(width=78):
"""Return formatted string for tutorial"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
help_md = get_tutorial_text()
help_md = strip_html_comments(help_md)
help_md = strip_md_links(help_md)
console.print(Markdown(help_md))
help_str = sio.getvalue()
sio.close()
return help_str
def get_tutorial_text():
"""Load tutorial text from file"""
# TODO: would be better to use importlib.abc.ResourceReader but I can't find a single example of how to do this
help_file = pathlib.Path(__file__).parent / "../tutorial.md"
with open(help_file, "r") as fd:
md = fd.read()
return md

26
osxphotos/cli/uuid.py Normal file
View File

@@ -0,0 +1,26 @@
"""uuid command for osxphotos CLI"""
import click
import photoscript
@click.command(name="uuid")
@click.pass_obj
@click.pass_context
@click.option(
"--filename",
"-f",
required=False,
is_flag=True,
default=False,
help="Include filename of selected photos in output",
)
def uuid(ctx, cli_obj, filename):
"""Print out unique IDs (UUID) of photos selected in Photos
Prints outs UUIDs in form suitable for --uuid-from-file and --skip-uuid-from-file
"""
for photo in photoscript.PhotosLibrary().selection:
if filename:
print(f"# {photo.filename}")
print(photo.uuid)

View File

@@ -1,4 +1,5 @@
""" ConfigOptions class to load/save config settings for osxphotos CLI """
import bitmath
import toml
__all__ = [
@@ -134,18 +135,12 @@ class ConfigOptions:
filename: full path to TOML file to write; filename will be overwritten if it exists
"""
# todo: add overwrite and option to merge contents already in TOML file (under different [section] with new content)
data = {}
for attr in sorted(self._attrs.keys()):
val = getattr(self, attr)
if val in [False, ()]:
val = None
else:
val = list(val) if type(val) == tuple else val
data[attr] = val
with open(filename, "w") as fd:
toml.dump({self._name: data}, fd)
toml.dump(self._get_toml_dict(), fd)
def write_to_str(self) -> str:
"""Write self to TOML str"""
return toml.dumps(self._get_toml_dict())
def load_from_file(self, filename, override=False):
"""Load options from a TOML file.
@@ -178,3 +173,21 @@ class ConfigOptions:
def asdict(self):
return {attr: getattr(self, attr) for attr in sorted(self._attrs.keys())}
def _get_toml_dict(self):
"""Return dict for writing to TOML file"""
data = {}
for attr in sorted(self._attrs.keys()):
val = getattr(self, attr)
if isinstance(val, bitmath.Bitmath):
val = int(val.to_Byte())
if val in [False, ()]:
val = None
else:
val = list(val) if type(val) == tuple else val
data[attr] = val
return {self._name: data}

View File

@@ -0,0 +1,46 @@
"""Error logger/crash reporter decorator"""
import datetime
import functools
import platform
import sys
import traceback
from rich import print
def crash_reporter(filename, message, title, postamble, *extra_args):
"""Create a crash dump file on error named filename
On error, create a crash dump file named filename with exception and stack trace.
message is printed to stderr
title is printed at beginning of crash dump file
postamble is printed to stderr after crash dump file is created
If extra_args is not None, any additional arguments to the function will be printed to the file.
"""
def decorated(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(message, file=sys.stderr)
print(f"[red]{e}[/red]", file=sys.stderr)
with open(filename, "w") as f:
f.write(f"{title}\n")
f.write(f"Created: {datetime.datetime.now()}\n")
f.write(f"Python version: {sys.version}\n")
f.write(f"Platform: {platform.platform()}\n")
f.write(f"sys.argv: {sys.argv}\n")
for arg in extra_args:
f.write(f"{arg}\n")
f.write(f"Error: {e}\n")
traceback.print_exc(file=f)
print(f"Crash log written to '{filename}'", file=sys.stderr)
print(f"{postamble}", file=sys.stderr)
sys.exit(1)
return wrapped
return decorated

View File

@@ -69,6 +69,8 @@ def unescape_str(s):
"""unescape an HTML string returned by exiftool -E"""
if type(s) != str:
return s
# avoid " in values which result in json.loads() throwing an exception, #636
s = s.replace("&quot;", '\\"')
return html.unescape(s)
@@ -105,7 +107,8 @@ class _ExifToolProc:
def __init__(self, exiftool=None):
"""construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it)"""
exiftool: optional path to exiftool binary (if not provided, will search path to find it)
"""
if hasattr(self, "_process_running") and self._process_running:
# already running
@@ -115,7 +118,6 @@ class _ExifToolProc:
f"ignoring exiftool={exiftool}"
)
return
self._process_running = False
self._exiftool = exiftool or get_exiftool_path()
self._start_proc()
@@ -147,6 +149,9 @@ class _ExifToolProc:
return
# open exiftool process
# make sure /usr/bin at start of path so exiftool can find xattr (see #636)
env = os.environ.copy()
env["PATH"] = f'/usr/bin/:{env["PATH"]}'
self._process = subprocess.Popen(
[
self._exiftool,
@@ -163,6 +168,7 @@ class _ExifToolProc:
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
self._process_running = True
@@ -362,6 +368,7 @@ class ExifTool:
error = "" if error == b"" else error.decode("utf-8")
self.warning = warning
self.error = error
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN], warning, error
@property
@@ -393,6 +400,7 @@ class ExifTool:
except Exception as e:
# will fail with some commands, e.g --ext AVI which produces
# 'No file with specified extension' instead of json
logging.warning(f"error loading json returned by exiftool: {e} {json_str}")
return dict()
exifdict = exifdict[0]
if not tag_groups:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
""" Utility functions for working with export_db """
import datetime
import os
import pathlib
import sqlite3
from typing import Callable, Optional, Tuple, Union
import toml
from rich import print
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__
from .utils import noop
from .export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
from .fileutil import FileUtil
from .photosdb import PhotosDB
__all__ = [
"export_db_check_signatures",
"export_db_get_last_run",
"export_db_get_version",
"export_db_save_config_to_file",
"export_db_touch_files",
"export_db_update_signatures",
"export_db_vacuum",
]
def isotime_from_ts(ts: int) -> str:
"""Convert timestamp to ISO 8601 time string"""
return datetime.datetime.fromtimestamp(ts).isoformat()
def export_db_get_version(
dbfile: Union[str, pathlib.Path]
) -> Tuple[Optional[int], Optional[int]]:
"""returns version from export database as tuple of (osxphotos version, export_db version)"""
conn = sqlite3.connect(str(dbfile))
c = conn.cursor()
row = c.execute(
"SELECT osxphotos, exportdb FROM version ORDER BY id DESC LIMIT 1;"
).fetchone()
if row:
return (row[0], row[1])
return (None, None)
def export_db_vacuum(dbfile: Union[str, pathlib.Path]) -> None:
"""Vacuum export database"""
conn = sqlite3.connect(str(dbfile))
c = conn.cursor()
c.execute("VACUUM;")
conn.commit()
def export_db_update_signatures(
dbfile: Union[str, pathlib.Path],
export_dir: Union[str, pathlib.Path],
verbose_: Callable = noop,
dry_run: bool = False,
) -> Tuple[int, int]:
"""Update signatures for all files found in the export database to match what's on disk
Returns: tuple of (updated, skipped)
"""
export_dir = pathlib.Path(export_dir)
fileutil = FileUtil
conn = sqlite3.connect(str(dbfile))
c = conn.cursor()
c.execute("SELECT filepath_normalized, filepath FROM export_data;")
rows = c.fetchall()
updated = 0
skipped = 0
for row in rows:
filepath_normalized = row[0]
filepath = row[1]
filepath = export_dir / filepath
if not os.path.exists(filepath):
skipped += 1
verbose_(f"[dark_orange]Skipping missing file[/dark_orange]: '{filepath}'")
continue
updated += 1
file_sig = fileutil.file_sig(filepath)
verbose_(f"[green]Updating signature for[/green]: '{filepath}'")
if not dry_run:
c.execute(
"UPDATE export_data SET dest_mode = ?, dest_size = ?, dest_mtime = ? WHERE filepath_normalized = ?;",
(file_sig[0], file_sig[1], file_sig[2], filepath_normalized),
)
if not dry_run:
conn.commit()
return (updated, skipped)
def export_db_get_last_run(
export_db: Union[str, pathlib.Path]
) -> Tuple[Optional[str], Optional[str]]:
"""Get last run from export database"""
conn = sqlite3.connect(str(export_db))
c = conn.cursor()
row = c.execute(
"SELECT datetime, args FROM runs ORDER BY id DESC LIMIT 1;"
).fetchone()
if row:
return row[0], row[1]
return None, None
def export_db_save_config_to_file(
export_db: Union[str, pathlib.Path], config_file: Union[str, pathlib.Path]
) -> None:
"""Save export_db last run config to file"""
export_db = pathlib.Path(export_db)
config_file = pathlib.Path(config_file)
conn = sqlite3.connect(str(export_db))
c = conn.cursor()
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
if not row:
return ValueError("No config found in export_db")
with config_file.open("w") as f:
f.write(row[0])
def export_db_check_signatures(
dbfile: Union[str, pathlib.Path],
export_dir: Union[str, pathlib.Path],
verbose_: Callable = noop,
) -> Tuple[int, int, int]:
"""Check signatures for all files found in the export database to verify what matches the on disk files
Returns: tuple of (updated, skipped)
"""
export_dir = pathlib.Path(export_dir)
fileutil = FileUtil
conn = sqlite3.connect(str(dbfile))
c = conn.cursor()
c.execute("SELECT filepath_normalized, filepath FROM export_data;")
rows = c.fetchall()
exportdb = ExportDB(dbfile, export_dir)
matched = 0
notmatched = 0
skipped = 0
for row in rows:
filepath_normalized = row[0]
filepath = row[1]
filepath = export_dir / filepath
if not filepath.exists():
skipped += 1
verbose_(f"[dark_orange]Skipping missing file[/dark_orange]: '{filepath}'")
continue
file_sig = fileutil.file_sig(filepath)
file_rec = exportdb.get_file_record(filepath)
if file_rec.dest_sig == file_sig:
matched += 1
verbose_(f"[green]Signatures matched[/green]: '{filepath}'")
else:
notmatched += 1
verbose_(f"[deep_pink3]Signatures do not match[/deep_pink3]: '{filepath}'")
return (matched, notmatched, skipped)
def export_db_touch_files(
dbfile: Union[str, pathlib.Path],
export_dir: Union[str, pathlib.Path],
verbose_: Callable = noop,
dry_run: bool = False,
) -> Tuple[int, int, int]:
"""Touch files on disk to match the Photos library created date
Returns: tuple of (touched, not_touched, skipped)
"""
export_dir = pathlib.Path(export_dir)
# open and close exportdb to ensure it gets migrated
exportdb = ExportDB(dbfile, export_dir)
upgraded = exportdb.was_upgraded
if upgraded:
verbose_(
f"Upgraded export database {dbfile} from version {upgraded[0]} to {upgraded[1]}"
)
exportdb.close()
conn = sqlite3.connect(str(dbfile))
c = conn.cursor()
# get most recent config
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
if row:
config = toml.loads(row[0])
try:
photos_db_path = config["export"].get("db", None)
except KeyError:
photos_db_path = None
else:
# TODO: parse the runs table to get the last --db
# in the mean time, photos_db_path = None will use the default library
photos_db_path = None
photosdb = PhotosDB(dbfile=photos_db_path, verbose=verbose_)
exportdb = ExportDB(dbfile, export_dir)
c.execute(
"SELECT filepath_normalized, filepath, uuid, dest_mode, dest_size FROM export_data;"
)
rows = c.fetchall()
touched = 0
not_touched = 0
skipped = 0
for row in rows:
filepath_normalized = row[0]
filepath = row[1]
filepath = export_dir / filepath
uuid = row[2]
dest_mode = row[3]
dest_size = row[4]
if not filepath.exists():
skipped += 1
verbose_(
f"[dark_orange]Skipping missing file (not in export directory)[/dark_orange]: '{filepath}'"
)
continue
photo = photosdb.get_photo(uuid)
if not photo:
skipped += 1
verbose_(
f"[dark_orange]Skipping missing photo (did not find in Photos Library)[/dark_orange]: '{filepath}' ({uuid})"
)
continue
ts = int(photo.date.timestamp())
stat = os.stat(str(filepath))
mtime = stat.st_mtime
if mtime == ts:
not_touched += 1
verbose_(
f"[green]Skipping file (timestamp matches)[/green]: '{filepath}' [dodger_blue1]{isotime_from_ts(ts)} ({ts})[/dodger_blue1]"
)
continue
touched += 1
verbose_(
f"[deep_pink3]Touching file[/deep_pink3]: '{filepath}' "
f"[dodger_blue1]{isotime_from_ts(mtime)} ({mtime}) -> {isotime_from_ts(ts)} ({ts})[/dodger_blue1]"
)
if not dry_run:
os.utime(str(filepath), (ts, ts))
rec = exportdb.get_file_record(filepath)
rec.dest_sig = (dest_mode, dest_size, ts)
return (touched, not_touched, skipped)

View File

@@ -143,7 +143,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod
def utime(cls, path, times):
"""Set the access and modified time of path."""
os.utime(path, times)
os.utime(path, times=times)
@classmethod
def cmp(cls, f1, f2, mtime1=None):
@@ -187,7 +187,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod
def file_sig(cls, f1):
"""return os.stat signature for file f1"""
"""return os.stat signature for file f1 as tuple of (mode, size, mtime)"""
return cls._sig(os.stat(f1))
@classmethod

View File

@@ -33,7 +33,7 @@ from ._constants import (
from ._version import __version__
from .datetime_utils import datetime_tz_to_utc
from .exiftool import ExifTool, exiftool_can_write
from .export_db import ExportDB_ABC, ExportDBNoOp
from .export_db import ExportDB, ExportDBTemp
from .fileutil import FileUtil
from .photokit import (
PHOTOS_VERSION_CURRENT,
@@ -81,7 +81,8 @@ class ExportOptions:
exiftool_flags (list of str): 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
export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
export_db: (ExportDB_ABC): instance of a class that conforms to ExportDB_ABC 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
face_regions: (bool, default=True): if True, will export face regions
fileutil: (FileUtilABC): class that conforms to FileUtilABC with various file utilities
force_update: (bool, default=False): if True, will export photo if any metadata has changed but export otherwise would not be triggered (e.g. metadata changed but not using exiftool)
ignore_date_modified (bool): for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
@@ -127,7 +128,8 @@ class ExportOptions:
exiftool_flags: Optional[List] = None
exiftool: bool = False
export_as_hardlink: bool = False
export_db: Optional[ExportDB_ABC] = None
export_db: Optional[ExportDB] = None
face_regions: bool = True
fileutil: Optional[FileUtil] = None
force_update: bool = False
ignore_date_modified: bool = False
@@ -162,6 +164,12 @@ class ExportOptions:
def asdict(self):
return asdict(self)
@property
def bit_flags(self):
"""Return bit flags representing options that affect export"""
# currently only exiftool makes a difference
return self.exiftool << 1
class StagedFiles:
"""Represents files staged for export"""
@@ -401,8 +409,8 @@ class PhotoExporter:
"Cannot use export_as_hardlink with download_missing or use_photos_export"
)
# when called from export(), won't get an export_db, so use no-op version
options.export_db = options.export_db or ExportDBNoOp()
# when called from export(), won't get an export_db, so use temp version
options.export_db = options.export_db or ExportDBTemp()
# ensure there's a FileUtil class to use
options.fileutil = options.fileutil or FileUtil
@@ -441,6 +449,7 @@ class PhotoExporter:
# get the right destination path depending on options.update, etc.
dest = self._get_dest_path(src, dest, options)
self._render_options.filepath = str(dest)
all_results = ExportResults()
@@ -543,23 +552,24 @@ class PhotoExporter:
all_results += self._write_sidecar_files(dest=dest, options=options)
if options.touch_file:
all_results += self._touch_files(all_results, options)
return all_results
def _touch_files(
self, results: ExportResults, options: ExportOptions
) -> ExportResults:
"""touch file date/time to match photo creation date/time"""
def _touch_files(self, touch_files: List, options: ExportOptions) -> ExportResults:
"""touch file date/time to match photo creation date/time; only touches files if needed"""
fileutil = options.fileutil
touch_files = set(results.to_touch)
touch_results = ExportResults()
for touch_file in touch_files:
touch_results = []
for touch_file in set(touch_files):
ts = int(self.photo.date.timestamp())
fileutil.utime(touch_file, (ts, ts))
touch_results.touched.append(touch_file)
return touch_results
try:
stat = os.stat(touch_file)
if stat.st_mtime != ts:
fileutil.utime(touch_file, (ts, ts))
touch_results.append(touch_file)
except FileNotFoundError as e:
# ignore errors if in dry_run as file may not be present
if not options.dry_run:
raise e from e
return ExportResults(touched=touch_results)
def _get_edited_filename(self, original_filename):
"""Return the filename for the exported edited photo
@@ -608,21 +618,11 @@ class PhotoExporter:
):
return pathlib.Path(increment_filename(dest))
# if update and file exists, need to check to see if it's the write file by checking export db
# if update and file exists, need to check to see if it's the right file by checking export db
if (options.update or options.force_update) and dest.exists() and src:
export_db = options.export_db
fileutil = options.fileutil
# destination exists, check to see if destination is the right UUID
dest_uuid = export_db.get_uuid_for_file(dest)
if dest_uuid is None and fileutil.cmp(src, dest):
# might be exporting into a pre-ExportDB folder or the DB got deleted
dest_uuid = self.photo.uuid
export_db.set_data(
filename=dest,
uuid=self.photo.uuid,
orig_stat=fileutil.file_sig(dest),
info_json=self.photo.json(),
)
if dest_uuid != self.photo.uuid:
# not the right file, find the right one
# find files that match "dest_name (*.ext" (e.g. "dest_name (1).jpg", "dest_name (2).jpg)", ...)
@@ -637,16 +637,6 @@ class PhotoExporter:
if dest_uuid == self.photo.uuid:
dest = pathlib.Path(file_)
break
elif dest_uuid is None and fileutil.cmp(src, file_):
# files match, update the UUID
dest = pathlib.Path(file_)
export_db.set_data(
filename=dest,
uuid=self.photo.uuid,
orig_stat=fileutil.file_sig(dest),
info_json=self.photo.json(),
)
break
else:
# increment the destination file
dest = pathlib.Path(increment_filename(dest))
@@ -654,6 +644,57 @@ class PhotoExporter:
# either dest was updated in the if clause above or not updated at all
return dest
def _should_update_photo(
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
) -> bool:
"""Return True if photo should be updated, else False"""
export_db = options.export_db
fileutil = options.fileutil
file_record = export_db.get_file_record(dest)
if not file_record:
# photo doesn't exist in database, should update
return True
if options.export_as_hardlink and not dest.samefile(src):
# different files, should update
return True
if not options.export_as_hardlink and dest.samefile(src):
# same file but not exporting as hardlink, should update
return True
if not options.ignore_signature and not fileutil.cmp_file_sig(
dest, file_record.dest_sig
):
# destination file doesn't match what was last exported
return True
if file_record.export_options != options.bit_flags:
# exporting with different set of options (e.g. exiftool), should update
# 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
# as it'll be None and bit_flags will be an int
return True
if options.exiftool:
current_exifdata = self._exiftool_json_sidecar(options=options)
return current_exifdata != file_record.exifdata
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
return True
if options.force_update:
current_digest = hexdigest(self.photo.json())
if current_digest != file_record.digest:
# metadata in Photos changed, force update
return True
# photo should not be updated
return False
def _stage_photos_for_export(self, options: ExportOptions) -> StagedFiles:
"""Stages photos for export
@@ -987,11 +1028,8 @@ class PhotoExporter:
update_updated_files = []
update_new_files = []
update_skipped_files = [] # skip files that are already up to date
touched_files = []
converted_to_jpeg_files = []
exif_results = ExportResults()
converted_stat = None
edited_stat = None
dest_str = str(dest)
dest_exists = dest.exists()
@@ -1000,98 +1038,25 @@ class PhotoExporter:
export_db = options.export_db
if options.update or options.force_update: # updating
cmp_touch, cmp_orig = False, False
if dest_exists:
# update, destination exists, but we might not need to replace it...
if options.exiftool:
sig_exif = export_db.get_stat_exif_for_file(dest_str)
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_exif)
if cmp_orig:
# if signatures match also need to compare exifdata to see if metadata changed
cmp_orig = not self._should_run_exiftool(dest_str, options)
sig_exif = (
sig_exif[0],
sig_exif[1],
int(self.photo.date.timestamp()),
)
cmp_touch = fileutil.cmp_file_sig(dest_str, sig_exif)
elif options.convert_to_jpeg:
sig_converted = export_db.get_stat_converted_for_file(dest_str)
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_converted)
sig_converted = (
sig_converted[0],
sig_converted[1],
int(self.photo.date.timestamp()),
)
cmp_touch = fileutil.cmp_file_sig(dest_str, sig_converted)
else:
cmp_orig = options.ignore_signature or fileutil.cmp(src, dest)
cmp_touch = fileutil.cmp(
src, dest, mtime1=int(self.photo.date.timestamp())
)
if options.force_update:
# need to also check the photo's metadata to that in the database
# and if anything changed, we need to update the file
# ony the hex digest of the metadata is stored in the database
photo_digest = hexdigest(self.photo.json())
db_digest = export_db.get_metadata_for_file(dest_str)
cmp_orig = photo_digest == db_digest
sig_cmp = cmp_touch if options.touch_file else cmp_orig
if options.edited:
# requested edited version of photo
# need to see if edited version in Photos library has changed
# (e.g. it's been edited again)
sig_edited = export_db.get_stat_edited_for_file(dest_str)
cmp_edited = (
fileutil.cmp_file_sig(src, sig_edited)
if sig_edited != (None, None, None)
else False
)
sig_cmp = sig_cmp and (options.force_update or cmp_edited)
if (options.export_as_hardlink and dest.samefile(src)) or (
not options.export_as_hardlink
and not dest.samefile(src)
and sig_cmp
):
# destination exists and signatures match, skip it
update_skipped_files.append(dest_str)
elif options.touch_file and cmp_orig and not cmp_touch:
# destination exists, signature matches original but does not match expected touch time
# skip exporting but update touch time
update_skipped_files.append(dest_str)
touched_files.append(dest_str)
elif not options.touch_file and cmp_touch and not cmp_orig:
# destination exists, signature matches expected touch but not original
# user likely exported with touch_file and is now exporting without touch_file
# don't update the file because it's same but leave touch time
update_skipped_files.append(dest_str)
else:
# destination exists but is different
if self._should_update_photo(src, dest, options):
update_updated_files.append(dest_str)
if options.touch_file:
touched_files.append(dest_str)
else:
update_skipped_files.append(dest_str)
else:
# update, destination doesn't exist (new file)
update_new_files.append(dest_str)
if options.touch_file:
touched_files.append(dest_str)
else:
# not update, export the file
exported_files.append(dest_str)
if options.touch_file:
sig = fileutil.file_sig(src)
sig = (sig[0], sig[1], int(self.photo.date.timestamp()))
if not fileutil.cmp_file_sig(src, sig):
touched_files.append(dest_str)
if not update_skipped_files:
# have file to export
edited_stat = (
fileutil.file_sig(src) if options.edited else (None, None, None)
export_files = update_new_files + update_updated_files + exported_files
for export_dest in export_files:
# set src_sig before any modifications by convert_to_jpeg or exiftool
export_record = export_db.create_or_get_file_record(
export_dest, self.photo.uuid
)
export_record.src_sig = fileutil.file_sig(src)
if dest_exists and any(
[options.overwrite, options.update, options.force_update]
):
@@ -1121,7 +1086,6 @@ class PhotoExporter:
src, tmp_file, compression_quality=options.jpeg_quality
)
src = tmp_file
converted_stat = fileutil.file_sig(tmp_file)
converted_to_jpeg_files.append(dest_str)
if options.exiftool:
@@ -1137,20 +1101,7 @@ class PhotoExporter:
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
) from e
json_info = self.photo.json()
# don't set the metadata digest if not force_update so that future use of force_update catches metadata change
metadata_digest = hexdigest(json_info) if options.force_update else None
export_db.set_data(
filename=dest_str,
uuid=self.photo.uuid,
orig_stat=fileutil.file_sig(dest_str),
converted_stat=converted_stat,
edited_stat=edited_stat,
info_json=json_info,
metadata=metadata_digest,
)
return ExportResults(
results = ExportResults(
converted_to_jpeg=converted_to_jpeg_files,
error=exif_results.error,
exif_updated=exif_results.exif_updated,
@@ -1159,10 +1110,34 @@ class PhotoExporter:
exported=exported_files + update_new_files + update_updated_files,
new=update_new_files,
skipped=update_skipped_files,
to_touch=touched_files,
updated=update_updated_files,
)
# touch files if needed
if options.touch_file:
results += self._touch_files(
exported_files
+ update_new_files
+ update_updated_files
+ update_skipped_files,
options,
)
# set data in the database
with export_db.create_or_get_file_record(dest_str, self.photo.uuid) as rec:
photoinfo = self.photo.json()
rec.photoinfo = photoinfo
rec.export_options = options.bit_flags
# don't set src_sig as that is set above before any modifications by convert_to_jpeg or exiftool
if not options.ignore_signature:
rec.dest_sig = fileutil.file_sig(dest)
if options.exiftool:
rec.exifdata = self._exiftool_json_sidecar(options)
if options.force_update:
rec.digest = hexdigest(photoinfo)
return results
def _write_sidecar_files(
self,
dest: pathlib.Path,
@@ -1243,8 +1218,8 @@ class PhotoExporter:
sidecar_type = data[4]
sidecar_digest = hexdigest(sidecar_str)
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
sidecar_filename
sidecar_record = export_db.create_or_get_file_record(
sidecar_filename, self.photo.uuid
)
write_sidecar = (
not (options.update or options.force_update)
@@ -1254,8 +1229,10 @@ class PhotoExporter:
)
or (
(options.update or options.force_update)
and (sidecar_digest != old_sidecar_digest)
or not fileutil.cmp_file_sig(sidecar_filename, sidecar_sig)
and (sidecar_digest != sidecar_record.digest)
or not fileutil.cmp_file_sig(
sidecar_filename, sidecar_record.dest_sig
)
)
)
if write_sidecar:
@@ -1263,16 +1240,13 @@ class PhotoExporter:
files_written.append(str(sidecar_filename))
if not options.dry_run:
self._write_sidecar(sidecar_filename, sidecar_str)
export_db.set_sidecar_for_file(
sidecar_filename,
sidecar_digest,
fileutil.file_sig(sidecar_filename),
)
sidecar_record.digest = sidecar_digest
sidecar_record.dest_sig = fileutil.file_sig(sidecar_filename)
else:
verbose(f"Skipped up to date {sidecar_type} sidecar {sidecar_filename}")
files_skipped.append(str(sidecar_filename))
return ExportResults(
results = ExportResults(
sidecar_json_written=sidecar_json_files_written,
sidecar_json_skipped=sidecar_json_files_skipped,
sidecar_exiftool_written=sidecar_exiftool_files_written,
@@ -1281,6 +1255,26 @@ class PhotoExporter:
sidecar_xmp_skipped=sidecar_xmp_files_skipped,
)
if options.touch_file:
all_sidecars = (
sidecar_json_files_written
+ sidecar_exiftool_files_written
+ sidecar_xmp_files_written
+ sidecar_json_files_skipped
+ sidecar_exiftool_files_skipped
+ sidecar_xmp_files_skipped
)
results += self._touch_files(all_sidecars, options)
# update destination signatures in database
for sidecar_filename in all_sidecars:
sidecar_record = export_db.create_or_get_file_record(
sidecar_filename, self.photo.uuid
)
sidecar_record.dest_sig = fileutil.file_sig(sidecar_filename)
return results
def _write_exif_metadata_to_file(
self,
src,
@@ -1295,10 +1289,7 @@ class PhotoExporter:
local machine prior to being copied to the export destination which may be on a
network drive or other slower external storage."""
export_db = options.export_db
fileutil = options.fileutil
verbose = options.verbose or self._verbose
exiftool_results = ExportResults()
# don't try to write if unsupported file type for exiftool
@@ -1310,53 +1301,35 @@ class PhotoExporter:
)
)
# set file signature so the file doesn't get re-exported with --update
export_db.set_data(
dest,
uuid=self.photo.uuid,
exif_stat=fileutil.file_sig(src),
exif_json=self._exiftool_json_sidecar(options=options),
)
return exiftool_results
# determine if we need to write the exif metadata
# if we are not updating, we always write
# else, need to check the database to determine if we need to write
run_exiftool = self._should_run_exiftool(dest, options)
if run_exiftool:
verbose(f"Writing metadata with exiftool for {pathlib.Path(dest).name}")
if not options.dry_run:
warning_, error_ = self._write_exif_data(src, options=options)
if warning_:
exiftool_results.exiftool_warning.append((dest, warning_))
if error_:
exiftool_results.exiftool_error.append((dest, error_))
exiftool_results.error.append((dest, error_))
verbose(f"Writing metadata with exiftool for {pathlib.Path(dest).name}")
if not options.dry_run:
warning_, error_ = self._write_exif_data(src, options=options)
if warning_:
exiftool_results.exiftool_warning.append((dest, warning_))
if error_:
exiftool_results.exiftool_error.append((dest, error_))
exiftool_results.error.append((dest, error_))
export_db.set_data(
dest,
uuid=self.photo.uuid,
exif_stat=fileutil.file_sig(src),
exif_json=self._exiftool_json_sidecar(options=options),
)
exiftool_results.exif_updated.append(dest)
exiftool_results.to_touch.append(dest)
else:
verbose(
f"Skipped up to date exiftool metadata for {pathlib.Path(dest).name}"
)
exiftool_results.exif_updated.append(dest)
exiftool_results.to_touch.append(dest)
return exiftool_results
def _should_run_exiftool(self, dest, options: ExportOptions) -> bool:
"""Return True if exiftool should be run to update metadata"""
run_exiftool = not (options.update or options.force_update)
run_exiftool = not options.update and not options.force_update
if options.update or options.force_update:
files_are_different = False
old_data = options.export_db.get_exifdata_for_file(dest)
exif_record = options.export_db.get_file_record(dest)
old_data = exif_record.exifdata if exif_record else None
if old_data is not None:
old_data = json.loads(old_data)[0]
current_data = json.loads(self._exiftool_json_sidecar(options=options))[
0
]
current_data = json.loads(self._exiftool_json_sidecar(options=options))
current_data = current_data[0]
if old_data != current_data:
files_are_different = True
@@ -1539,6 +1512,9 @@ class PhotoExporter:
person_list = sorted(list(set(person_list)))
exif["XMP:PersonInImage"] = person_list.copy()
if options.face_regions and self.photo.face_info:
exif.update(self._get_mwg_face_regions_exiftool())
# if self.favorite():
# exif["Rating"] = 5
@@ -1621,6 +1597,42 @@ class PhotoExporter:
return exif
def _get_mwg_face_regions_exiftool(self):
"""Return a dict with MWG face regions for use by exiftool"""
if self.photo.orientation in [5, 6, 7, 8]:
w = self.photo.height
h = self.photo.width
else:
w = self.photo.width
h = self.photo.height
exif = {}
exif["XMP:RegionAppliedToDimensionsW"] = w
exif["XMP:RegionAppliedToDimensionsH"] = h
exif["XMP:RegionAppliedToDimensionsUnit"] = "pixel"
exif["XMP:RegionName"] = []
exif["XMP:RegionType"] = []
exif["XMP:RegionAreaX"] = []
exif["XMP:RegionAreaY"] = []
exif["XMP:RegionAreaW"] = []
exif["XMP:RegionAreaH"] = []
exif["XMP:RegionAreaUnit"] = []
exif["XMP:RegionPersonDisplayName"] = []
# exif["XMP:RegionRectangle"] = []
for face in self.photo.face_info:
if not face.name:
continue
area = face.mwg_rs_area
exif["XMP:RegionName"].append(face.name)
exif["XMP:RegionType"].append("Face")
exif["XMP:RegionAreaX"].append(area.x)
exif["XMP:RegionAreaY"].append(area.y)
exif["XMP:RegionAreaW"].append(area.w)
exif["XMP:RegionAreaH"].append(area.h)
exif["XMP:RegionAreaUnit"].append("normalized")
exif["XMP:RegionPersonDisplayName"].append(face.name)
# exif["XMP:RegionRectangle"].append(f"{area.x},{area.y},{area.h},{area.w}")
return exif
def _get_exif_keywords(self):
"""returns list of keywords found in the file's exif metadata"""
keywords = []

View File

@@ -54,7 +54,7 @@ from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo
from .text_detection import detect_text
from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _debug, _get_resource_loc, list_directory
from .utils import _debug, _get_resource_loc, list_directory, _debug
__all__ = ["PhotoInfo", "PhotoInfoNone"]
@@ -588,6 +588,7 @@ class PhotoInfo:
@property
def ismissing(self):
"""returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
do not immediately get written to disk. In particular, I've noticed that downloading
an image from the cloud does not force the database to be updated until something else
@@ -1488,7 +1489,8 @@ class PhotoInfo:
description_template: string; optional template string that will be rendered for use as photo description
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
Returns: list of photos exported
Returns:
list of photos exported
"""
exporter = PhotoExporter(self)

View File

@@ -4,7 +4,7 @@
import logging
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _db_is_locked, _debug, _open_sql_file
from ..utils import _db_is_locked, _open_sql_file
from .photosdb_utils import get_db_version

View File

@@ -10,7 +10,7 @@ import uuid as uuidlib
from pprint import pformat
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL
from ..utils import _db_is_locked, _debug, _open_sql_file, normalize_unicode
from ..utils import _db_is_locked, _open_sql_file, normalize_unicode
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
@@ -139,17 +139,6 @@ def _process_searchinfo(self):
_db_searchinfo_labels[label] = [uuid]
_db_searchinfo_labels_normalized[label_norm] = [uuid]
if _debug():
logging.debug(
"_db_searchinfo_categories: \n" + pformat(self._db_searchinfo_categories)
)
logging.debug("_db_searchinfo_uuid: \n" + pformat(self._db_searchinfo_uuid))
logging.debug("_db_searchinfo_labels: \n" + pformat(self._db_searchinfo_labels))
logging.debug(
"_db_searchinfo_labels_normalized: \n"
+ pformat(self._db_searchinfo_labels_normalized)
)
conn.close()

View File

@@ -2456,7 +2456,6 @@ class PhotosDB:
verbose("Processing moments.")
self._process_moments()
# done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
def _process_moments(self):
@@ -3280,27 +3279,6 @@ class PhotosDB:
if options.to_time:
photos = [p for p in photos if p.date.time() <= options.to_time]
if options.burst_photos:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
if options.missing_bursts:
# include burst photos that are missing
photos.extend(burst.burst_photos)
else:
# don't include missing burst images (these can't be downloaded with AppleScript)
photos.extend([p for p in burst.burst_photos if not p.ismissing])
# remove duplicates as each burst photo in the set that's selected would
# result in the entire set being added above
# can't use set() because PhotoInfo not hashable
seen_uuids = {}
for p in photos:
if p.uuid in seen_uuids:
continue
seen_uuids[p.uuid] = p
photos = list(seen_uuids.values())
if name:
# search filename fields for text
# if more than one, find photos with all title values in filename
@@ -3451,6 +3429,28 @@ class PhotosDB:
for function in options.function:
photos = function[0](photos)
# burst should be checked last, ref #640
if options.burst_photos:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
if options.missing_bursts:
# include burst photos that are missing
photos.extend(burst.burst_photos)
else:
# don't include missing burst images (these can't be downloaded with AppleScript)
photos.extend([p for p in burst.burst_photos if not p.ismissing])
# remove duplicates as each burst photo in the set that's selected would
# result in the entire set being added above
# can't use set() because PhotoInfo not hashable
seen_uuids = {}
for p in photos:
if p.uuid in seen_uuids:
continue
seen_uuids[p.uuid] = p
photos = list(seen_uuids.values())
return photos
def execute(self, sql):

View File

@@ -99,7 +99,7 @@ OPERATOR:
PathSep:
(
"("
(value=/[^\(\)\{\}]{0,1}/)?
(value=/[^\(\)\{\}]+/)?
")"
)?
;

View File

@@ -29,7 +29,7 @@ def sqlgrep(
flags = re.IGNORECASE if ignore_case else 0
try:
with sqlite3.connect(f"file:{filename}?mode=ro", uri=True) as conn:
regex = re.compile(r"(" + pattern + r")", flags=flags)
regex = re.compile(f'({pattern})', flags=flags)
filename_header = f"{filename}: " if print_filename else ""
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
@@ -54,4 +54,4 @@ def sqlgrep(
field_value,
]
except sqlite3.DatabaseError as e:
raise sqlite3.DatabaseError(f"{filename}: {e}")
raise sqlite3.DatabaseError(f"{filename}: {e}") from e

View File

@@ -103,6 +103,8 @@
% if photo.face_info:
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>${photo.width if photo.orientation in [5, 6, 7, 8] else photo.height}</stDim:h>
<stDim:w>${photo.height if photo.orientation in [5, 6, 7, 8] else photo.width}</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -103,6 +103,8 @@
% if photo.face_info:
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>${photo.width if photo.orientation in [5, 6, 7, 8] else photo.height}</stDim:h>
<stDim:w>${photo.height if photo.orientation in [5, 6, 7, 8] else photo.width}</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1,26 +1,25 @@
Click>=8.0.1,<9.0
Mako>=1.1.4,<1.2.0
PyYAML>=5.4.1,<6.0.0
bitmath>=1.3.3.1,<1.4.0.0
bpylist2==3.0.2
dataclasses==0.7;python_version<'3.7'
Click>=8.0.4,<9.0
Mako>=1.1.4,<1.2.0
more-itertools>=8.8.0,<9.0.0
objexplore>=1.5.5,<1.6.0
objexplore>=1.6.3,<2.0.0
osxmetadata>=0.99.34,<1.0.0
pathvalidate>=2.4.1,<2.5.0
photoscript>=0.1.4,<0.2.0
ptpython>=3.0.20,<3.1.0
pyobjc-core>=7.3,<9.0
pyobjc-framework-AVFoundation>=7.3,<9.0
pyobjc-framework-AppleScriptKit>=7.3,<9.0
pyobjc-framework-AppleScriptObjC>=7.3,<9.0
pyobjc-framework-AVFoundation>=7.3,<9.0
pyobjc-framework-Cocoa>=7.3,<9.0
pyobjc-framework-CoreServices>=7.2,<9.0
pyobjc-framework-Metal>=7.3,<9.0
pyobjc-framework-Photos>=7.3,<9.0
pyobjc-framework-Quartz>=7.3,<9.0
pyobjc-framework-Vision>=7.3,<9.0
rich>=10.6.0,<=11.0.0
PyYAML>=5.4.1,<6.0.0
rich>=11.2.0,<12.0.0
textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0
wurlitzer>=2.1.0,<2.2.0

View File

@@ -67,21 +67,19 @@ setup(
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
"Click>=8.0.1,<9.0",
"Click>=8.0.4,<9.0",
"Mako>=1.1.4,<1.2.0",
"PyYAML>=5.4.1,<5.5.0",
"bitmath>=1.3.3.1,<1.4.0.0",
"bpylist2==3.0.2",
"dataclasses==0.7;python_version<'3.7'",
"more-itertools>=8.8.0,<9.0.0",
"objexplore>=1.5.5,<1.6.0",
"objexplore>=1.6.3,<2.0.0",
"osxmetadata>=0.99.34,<1.0.0",
"pathvalidate>=2.4.1,<3.0.0",
"photoscript>=0.1.4,<0.2.0",
@@ -96,11 +94,11 @@ setup(
"pyobjc-framework-Photos>=7.3,<9.0",
"pyobjc-framework-Quartz>=7.3,<9.0",
"pyobjc-framework-Vision>=7.3,<9.0",
"rich>=10.6.0,<=11.0.0",
"rich>=11.2.0,<12.0.0",
"textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0",
"wurlitzer>=2.1.0,<3.0.0",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},
include_package_data=True,
)

View File

@@ -1,14 +1,18 @@
# Tests for osxphotos #
## Running Tests ##
Tests require pytest and pytest-mock:
`pip install pytest`
`pip install pytest-mock`
To set up a dev environment to work on osxphotos code or run tests follow these steps. This assumes you have python 3.7 or later installed. If you need to install python, you can do so with the XCode command lines tools (`xcode-select --install`) or from [python.org](https://www.python.org/downloads/macos/).
- `git clone git@github.com:RhetTbull/osxphotos.git`
- `cd osxphotos`
- `python3 -m venv venv`
- `source venv/bin/activate`
- `python3 -m pip install -r dev_requirements.txt`
- `python3 -m pip install -e .`
To run the tests, do the following from the main source folder:
`python -m pytest tests/`
`python3 -m pytest tests/`
Running the tests this way allows the library to be tested without installing it.
## Skipped Tests ##
A few tests will look for certain environment variables to determine if they should run.

View File

@@ -3,10 +3,9 @@ import os
import pathlib
import pytest
from applescript import AppleScript
from photoscript.utils import ditto
import osxphotos
from applescript import AppleScript
from osxphotos.exiftool import _ExifToolProc
@@ -124,3 +123,12 @@ def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str
"""Copy a photos library to a folder"""
ditto(photos_library_path, dest_path)
return dest_path
@pytest.fixture(scope="session", autouse=True)
def delete_crash_logs():
"""Delete left over crash logs from tests that were supposed to crash"""
yield
path = pathlib.Path(os.getcwd()) / "osxphotos_crash.log"
if path.is_file():
path.unlink()

View File

@@ -7,6 +7,7 @@
import pathlib
import osxphotos
from osxphotos.photoexporter import PhotoExporter, ExportOptions
PHOTOS_DB_15_7 = "./tests/Test-10.15.7.photoslibrary/database/photos.db"
PHOTOS_DB_14_6 = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
@@ -31,7 +32,7 @@ SIDECAR_DIR = "tests/sidecars"
def generate_sidecars(dbname, uuid_dict):
""" generate XMP and JSON sidecars for testing """
"""generate XMP and JSON sidecars for testing"""
photosdb = osxphotos.PhotosDB(dbname)
for _, uuid in uuid_dict.items():
@@ -39,7 +40,8 @@ def generate_sidecars(dbname, uuid_dict):
# plain xmp
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.xmp")
xmp = photo._xmp_sidecar()
exporter = PhotoExporter(photo)
xmp = exporter._xmp_sidecar()
with open(sidecar, "w") as file:
file.write(xmp)
@@ -47,63 +49,76 @@ def generate_sidecars(dbname, uuid_dict):
ext = osxphotos.uti.get_preferred_uti_extension(photo.uti)
ext = "jpg" if ext == "jpeg" else ext
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_ext.xmp")
xmp = photo._xmp_sidecar(extension=ext)
xmp = exporter._xmp_sidecar(extension=ext)
with open(sidecar, "w") as file:
file.write(xmp)
# persons_as_keywords
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_persons_as_keywords.xmp")
xmp = photo._xmp_sidecar(use_persons_as_keywords=True, extension=ext)
xmp = exporter._xmp_sidecar(
ExportOptions(use_persons_as_keywords=True), extension=ext
)
with open(sidecar, "w") as file:
file.write(xmp)
# albums_as_keywords
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_albums_as_keywords.xmp")
xmp = photo._xmp_sidecar(use_albums_as_keywords=True, extension=ext)
xmp = exporter._xmp_sidecar(
ExportOptions(use_albums_as_keywords=True), extension=ext
)
with open(sidecar, "w") as file:
file.write(xmp)
# keyword_template
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.xmp")
xmp = photo._xmp_sidecar(
keyword_template=["{created.year}", "{folder_album}"], extension=ext
xmp = exporter._xmp_sidecar(
ExportOptions(keyword_template=["{created.year}", "{folder_album}"]),
extension=ext,
)
with open(sidecar, "w") as file:
file.write(xmp)
# generate JSON files
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.json")
json_ = photo._exiftool_json_sidecar()
json_ = exporter._exiftool_json_sidecar()
with open(sidecar, "w") as file:
file.write(json_)
# no tag groups
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_no_tag_groups.json")
json_ = photo._exiftool_json_sidecar(tag_groups=False)
json_ = exporter._exiftool_json_sidecar(tag_groups=False)
with open(sidecar, "w") as file:
file.write(json_)
# ignore_date_modified
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_ignore_date_modified.json")
json_ = photo._exiftool_json_sidecar(ignore_date_modified=True)
json_ = exporter._exiftool_json_sidecar(
ExportOptions(ignore_date_modified=True)
)
with open(sidecar, "w") as file:
file.write(json_)
# keyword_template
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.json")
json_ = photo._exiftool_json_sidecar(keyword_template=["{folder_album}"])
json_ = exporter._exiftool_json_sidecar(
ExportOptions(keyword_template=["{folder_album}"])
)
with open(sidecar, "w") as file:
file.write(json_)
# persons_as_keywords
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_persons_as_keywords.json")
json_ = photo._exiftool_json_sidecar(use_persons_as_keywords=True)
json_ = exporter._exiftool_json_sidecar(
ExportOptions(use_persons_as_keywords=True)
)
with open(sidecar, "w") as file:
file.write(json_)
# albums_as_keywords
sidecar = str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_albums_as_keywords.json")
json_ = photo._exiftool_json_sidecar(use_albums_as_keywords=True)
json_ = exporter._exiftool_json_sidecar(
ExportOptions(use_albums_as_keywords=True)
)
with open(sidecar, "w") as file:
file.write(json_)

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "XMP:RegionAppliedToDimensionsW": 1365, "XMP:RegionAppliedToDimensionsH": 2048, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Katie"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.5832663178443909], "XMP:RegionAreaY": [0.27730926126241684], "XMP:RegionAreaW": [0.24365156888961792], "XMP:RegionAreaH": [0.16239472242887132], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -52,6 +52,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>2048</stDim:h>
<stDim:w>1365</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "XMP:RegionAppliedToDimensionsW": 1365, "XMP:RegionAppliedToDimensionsH": 2048, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Katie"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.5832663178443909], "XMP:RegionAreaY": [0.27730926126241684], "XMP:RegionAreaW": [0.24365156888961792], "XMP:RegionAreaH": [0.16239472242887132], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -58,6 +58,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>2048</stDim:h>
<stDim:w>1365</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -52,6 +52,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>2048</stDim:h>
<stDim:w>1365</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "XMP:RegionAppliedToDimensionsW": 1365, "XMP:RegionAppliedToDimensionsH": 2048, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Katie"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.5832663178443909], "XMP:RegionAreaY": [0.27730926126241684], "XMP:RegionAreaW": [0.24365156888961792], "XMP:RegionAreaH": [0.16239472242887132], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "XMP:RegionAppliedToDimensionsW": 1365, "XMP:RegionAppliedToDimensionsH": 2048, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Katie"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.5832663178443909], "XMP:RegionAreaY": [0.27730926126241684], "XMP:RegionAreaW": [0.24365156888961792], "XMP:RegionAreaH": [0.16239472242887132], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -60,6 +60,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>2048</stDim:h>
<stDim:w>1365</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Caption-Abstract": "Girl holding pumpkin", "Title": "I found one!", "ObjectName": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}]
[{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Caption-Abstract": "Girl holding pumpkin", "Title": "I found one!", "ObjectName": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "RegionAppliedToDimensionsW": 1365, "RegionAppliedToDimensionsH": 2048, "RegionAppliedToDimensionsUnit": "pixel", "RegionName": ["Katie"], "RegionType": ["Face"], "RegionAreaX": [0.5832663178443909], "RegionAreaY": [0.27730926126241684], "RegionAreaW": [0.24365156888961792], "RegionAreaH": [0.16239472242887132], "RegionAreaUnit": ["normalized"], "RegionPersonDisplayName": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "XMP:RegionAppliedToDimensionsW": 1365, "XMP:RegionAppliedToDimensionsH": 2048, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Katie"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.5832663178443909], "XMP:RegionAreaY": [0.27730926126241684], "XMP:RegionAreaW": [0.24365156888961792], "XMP:RegionAreaH": [0.16239472242887132], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]

View File

@@ -54,6 +54,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>2048</stDim:h>
<stDim:w>1365</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "XMP:RegionAppliedToDimensionsW": 1526, "XMP:RegionAppliedToDimensionsH": 1325, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Maria"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.40229974687099457], "XMP:RegionAreaY": [0.41379398107528687], "XMP:RegionAreaW": [0.3420099108278517], "XMP:RegionAreaH": [0.39389216899871826], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]

View File

@@ -52,6 +52,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1325</stDim:h>
<stDim:w>1526</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "XMP:RegionAppliedToDimensionsW": 1526, "XMP:RegionAppliedToDimensionsH": 1325, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Maria"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.40229974687099457], "XMP:RegionAreaY": [0.41379398107528687], "XMP:RegionAreaW": [0.3420099108278517], "XMP:RegionAreaH": [0.39389216899871826], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]

View File

@@ -52,6 +52,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1325</stDim:h>
<stDim:w>1526</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -52,6 +52,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1325</stDim:h>
<stDim:w>1526</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:04:15 14:40:24"}]
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "XMP:RegionAppliedToDimensionsW": 1526, "XMP:RegionAppliedToDimensionsH": 1325, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Maria"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.40229974687099457], "XMP:RegionAreaY": [0.41379398107528687], "XMP:RegionAreaW": [0.3420099108278517], "XMP:RegionAreaH": [0.39389216899871826], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:04:15 14:40:24"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["wedding"], "XMP:Subject": ["wedding"], "XMP:TagsList": ["wedding"], "XMP:PersonInImage": ["Maria"], "XMP:RegionAppliedToDimensionsW": 1526, "XMP:RegionAppliedToDimensionsH": 1325, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Maria"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.40229974687099457], "XMP:RegionAreaY": [0.41379398107528687], "XMP:RegionAreaW": [0.3420099108278517], "XMP:RegionAreaH": [0.39389216899871826], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]

View File

@@ -54,6 +54,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1325</stDim:h>
<stDim:w>1526</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"ImageDescription": "Bride Wedding day", "Description": "Bride Wedding day", "Caption-Abstract": "Bride Wedding day", "Keywords": ["wedding"], "Subject": ["wedding"], "TagsList": ["wedding"], "PersonInImage": ["Maria"], "DateTimeOriginal": "2019:04:15 14:40:24", "CreateDate": "2019:04:15 14:40:24", "OffsetTimeOriginal": "-04:00", "DateCreated": "2019:04:15", "TimeCreated": "14:40:24-04:00", "ModifyDate": "2019:11:27 01:30:16"}]
[{"ImageDescription": "Bride Wedding day", "Description": "Bride Wedding day", "Caption-Abstract": "Bride Wedding day", "Keywords": ["wedding"], "Subject": ["wedding"], "TagsList": ["wedding"], "PersonInImage": ["Maria"], "RegionAppliedToDimensionsW": 1526, "RegionAppliedToDimensionsH": 1325, "RegionAppliedToDimensionsUnit": "pixel", "RegionName": ["Maria"], "RegionType": ["Face"], "RegionAreaX": [0.40229974687099457], "RegionAreaY": [0.41379398107528687], "RegionAreaW": [0.3420099108278517], "RegionAreaH": [0.39389216899871826], "RegionAreaUnit": ["normalized"], "RegionPersonDisplayName": ["Maria"], "DateTimeOriginal": "2019:04:15 14:40:24", "CreateDate": "2019:04:15 14:40:24", "OffsetTimeOriginal": "-04:00", "DateCreated": "2019:04:15", "TimeCreated": "14:40:24-04:00", "ModifyDate": "2019:11:27 01:30:16"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["Maria", "wedding"], "XMP:Subject": ["Maria", "wedding"], "XMP:TagsList": ["Maria", "wedding"], "XMP:PersonInImage": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]
[{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", "IPTC:Caption-Abstract": "Bride Wedding day", "IPTC:Keywords": ["Maria", "wedding"], "XMP:Subject": ["Maria", "wedding"], "XMP:TagsList": ["Maria", "wedding"], "XMP:PersonInImage": ["Maria"], "XMP:RegionAppliedToDimensionsW": 1526, "XMP:RegionAppliedToDimensionsH": 1325, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Maria"], "XMP:RegionType": ["Face"], "XMP:RegionAreaX": [0.40229974687099457], "XMP:RegionAreaY": [0.41379398107528687], "XMP:RegionAreaW": [0.3420099108278517], "XMP:RegionAreaH": [0.39389216899871826], "XMP:RegionAreaUnit": ["normalized"], "XMP:RegionPersonDisplayName": ["Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", "EXIF:CreateDate": "2019:04:15 14:40:24", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2019:04:15", "IPTC:TimeCreated": "14:40:24-04:00", "EXIF:ModifyDate": "2019:11:27 01:30:16"}]

View File

@@ -54,6 +54,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1325</stDim:h>
<stDim:w>1526</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie", "Suzy"], "XMP:RegionAppliedToDimensionsW": 2048, "XMP:RegionAppliedToDimensionsH": 1365, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Suzy", "Katie"], "XMP:RegionType": ["Face", "Face"], "XMP:RegionAreaX": [0.3097837567329407, 0.7126731872558595], "XMP:RegionAreaY": [0.33553561195731163, 0.4239872685185184], "XMP:RegionAreaW": [0.07461518356649322, 0.05], "XMP:RegionAreaH": [0.1119501069188118, 0.07501831501831502], "XMP:RegionAreaUnit": ["normalized", "normalized"], "XMP:RegionPersonDisplayName": ["Suzy", "Katie"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

View File

@@ -53,6 +53,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1365</stDim:h>
<stDim:w>2048</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "XMP:RegionAppliedToDimensionsW": 2048, "XMP:RegionAppliedToDimensionsH": 1365, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Suzy", "Katie"], "XMP:RegionType": ["Face", "Face"], "XMP:RegionAreaX": [0.3097837567329407, 0.7126731872558595], "XMP:RegionAreaY": [0.33553561195731163, 0.4239872685185184], "XMP:RegionAreaW": [0.07461518356649322, 0.05], "XMP:RegionAreaH": [0.1119501069188118, 0.07501831501831502], "XMP:RegionAreaUnit": ["normalized", "normalized"], "XMP:RegionPersonDisplayName": ["Suzy", "Katie"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

View File

@@ -57,6 +57,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1365</stDim:h>
<stDim:w>2048</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -53,6 +53,8 @@
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
<mwg-rs:Regions rdf:parseType="Resource">
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
<stDim:h>1365</stDim:h>
<stDim:w>2048</stDim:w>
<stDim:unit>pixel</stDim:unit>
</mwg-rs:AppliedToDimensions>
<mwg-rs:RegionList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie", "Suzy"], "XMP:RegionAppliedToDimensionsW": 2048, "XMP:RegionAppliedToDimensionsH": 1365, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Suzy", "Katie"], "XMP:RegionType": ["Face", "Face"], "XMP:RegionAreaX": [0.3097837567329407, 0.7126731872558595], "XMP:RegionAreaY": [0.33553561195731163, 0.4239872685185184], "XMP:RegionAreaW": [0.07461518356649322, 0.05], "XMP:RegionAreaH": [0.1119501069188118, 0.07501831501831502], "XMP:RegionAreaUnit": ["normalized", "normalized"], "XMP:RegionPersonDisplayName": ["Suzy", "Katie"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "XMP:RegionAppliedToDimensionsW": 2048, "XMP:RegionAppliedToDimensionsH": 1365, "XMP:RegionAppliedToDimensionsUnit": "pixel", "XMP:RegionName": ["Suzy", "Katie"], "XMP:RegionType": ["Face", "Face"], "XMP:RegionAreaX": [0.3097837567329407, 0.7126731872558595], "XMP:RegionAreaY": [0.33553561195731163, 0.4239872685185184], "XMP:RegionAreaW": [0.07461518356649322, 0.05], "XMP:RegionAreaH": [0.1119501069188118, 0.07501831501831502], "XMP:RegionAreaUnit": ["normalized", "normalized"], "XMP:RegionPersonDisplayName": ["Suzy", "Katie"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

Some files were not shown because too many files have changed in this diff Show More