Compare commits

..

11 Commits

Author SHA1 Message Date
Rhet Turnbull
030191be96 Working on making export CLI threadsafe 2023-04-02 12:36:51 -07:00
Rhet Turnbull
81127b6d89 Added locking to exiftool 2023-04-01 19:07:36 -07:00
Rhet Turnbull
d8c7d45056 Updated ExifTool to use multiple processes 2023-04-01 19:00:01 -07:00
Rhet Turnbull
93d22c646f Updated example [skip ci] 2023-04-01 13:59:47 -07:00
Rhet Turnbull
77000d85c6 Updated example [skip ci] 2023-04-01 10:26:20 -07:00
Rhet Turnbull
005f821501 Updated CHANGELOG.md [skip ci] 2023-04-01 10:17:16 -07:00
Rhet Turnbull
622f8952b7 Release 0.59.0 2023-04-01 10:09:26 -07:00
Rhet Turnbull
b7816be459 Added concurrent export example 2023-04-01 10:03:05 -07:00
Rhet Turnbull
e7099d250b Concurrency refactor 999 (#1029)
* Working on making export threadsafe, #999

* Working on making export threadsafe, #999

* refactor for concurrent export, #999

* Fixed race condition in ExportRecord context manager
2023-04-01 09:39:08 -07:00
Rhet Turnbull
2c4d0f4546 Fixed help text 2023-03-14 12:26:19 -07:00
Rhet Turnbull
84954f4551 Updated CHANGELOG.md [skip ci] 2023-03-14 12:22:04 -07:00
53 changed files with 3072 additions and 1559 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.58.2
current_version = 0.59.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch}

View File

@@ -376,6 +376,73 @@ if __name__ == "__main__":
```
<!--[[[end]]]-->
## Concurrency
OSXPhotos is not currently compatible with multiprocessing as the `PhotosDB` class cannot be pickled which required
when sharing data between processes. Photos can be exported concurrently using separate threads, however, this is
only compatible with Python 3.11 and later. See [issue #999](https://github.com/RhetTbull/osxphotos/issues/999).
The reason for this is that internally, `PhotoExporter` uses a sqlite `ExportDB` database for managing the export,
even if you don't specify an export database. (In the case where you don't specify an export database, a temporary
in-memory database is created and then discard.) The python implementation of sqlite3 is not fully thread safe on
Python < 3.11.
For example, the following code will work on Python >= 3.11. This code is available in the `examples` directory as
[concurrent_export.py](https://github.com/RhetTbull/osxphotos/blob/main/examples/concurrent_export.py).
```python
"""Example for concurrent export of photos using osxphotos.PhotoExporter.export()
Note: concurrent export can only be used on Python 3.11 and later due to the way
python's sqlite3 module is implemented. See https://docs.python.org/3/library/sqlite3.html#sqlite3.threadsafety
for more information.
"""
import concurrent.futures
import os
import time
import click
import osxphotos
from osxphotos.cli import echo, query_command, verbose
@query_command()
@click.option(
"--workers",
metavar="WORKERS",
help="Maximum number of worker threads to use for export. "
"If not specified, it will default to the number of processors on the machine, multiplied by 5.",
type=int,
)
@click.argument(
"export_dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True),
)
def export(workers, export_dir, photos: list[osxphotos.PhotoInfo], **kwargs):
"""Export photos"""
workers = workers or os.cpu_count() * 5
echo(f"Exporting {len(photos)} photos to {export_dir} using {workers} workers")
start_t = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [
executor.submit(p.export, export_dir, f"{p.uuid}_{p.original_filename}")
for p in photos
]
exported = []
for future in concurrent.futures.as_completed(futures):
exported.extend(future.result())
end_t = time.perf_counter()
echo(
f"Exported {len(exported)} photos to {export_dir} in {end_t-start_t:.4f} seconds"
)
if __name__ == "__main__":
export()
```
## Package Interface
### PhotosDB
@@ -1523,12 +1590,14 @@ Returns full name of the album owner (person who shared the album) for shared al
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
#### `asdict()`
Returns a dictionary representation of the AlbumInfo object.
### ImportInfo
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
**Note**: Photos 5+ only. Not implemented for Photos version <= 4.
#### `uuid`
Returns the universally unique identifier (uuid) of the import session. This is how Photos keeps track of individual objects within the database.
@@ -1543,12 +1612,18 @@ Returns the creation date as a timezone aware datetime.datetime object of the im
#### `start_date`
Returns the start date as a timezone aware datetime.datetime object for when the import session bega.
Returns the start date as a timezone aware datetime.datetime object for when the import session began.
#### `end_date`
Returns the end date as a timezone aware datetime.datetime object for when the import session completed.
**Note**: On Photos <=4, `start_date` and `end_date` will be the same as `creation_date`.
#### `asdict()`
Returns a dictionary representation of the import session.
### ProjectInfo
PhotosDB.projcet_info returns a list of ProjectInfo objects. Each ProjectInfo object represents a project in the library. PhotoInfo.project_info returns a list of ProjectInfo objects for each project the photo is contained in.
@@ -1571,6 +1646,10 @@ Returns a list of [PhotoInfo](#photoinfo) objects representing each photo contai
Returns the creation date as a timezone aware datetime.datetime object of the project.
#### `asdict()`
Returns a dictionary representation of the ProjectInfo object.
### MomentInfo
PhotoInfo.moment_info return the MomentInfo object for the photo. The MomentInfo object contains information about the photo's moment as assigned by Photos. The MomentInfo object contains the following properties:
@@ -1661,6 +1740,10 @@ Returns album sort order (as `AlbumSortOrder` enum). On Photos <=4, always retu
Returns index of photo in album (based on album sort order).
#### `asdict()`
Returns a dictionary representation of the FolderInfo object.
**Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `subfolders` and `album_info` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example:
```pycon
@@ -2373,7 +2456,7 @@ cog.out(get_template_field_table())
|{cr}|A carriage return: '\r'|
|{crlf}|A carriage return + line feed: '\r\n'|
|{tab}|:A tab: '\t'|
|{osxphotos_version}|The osxphotos version, e.g. '0.58.2'|
|{osxphotos_version}|The osxphotos version, e.g. '0.59.0'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file.
## [v0.59.0](https://github.com/RhetTbull/osxphotos/compare/v0.58.2...v0.59.0)
### 1 April 2023
#### Added
- `PhotoInfo.export()` and `PhotoExporter.export()` now support exporting in concurrent threads on Python 3.11+. This applies only to the API. The `osxphotos export` CLI does not yet support concurrent export. See #999.
See example code in [concurrent_export.py](https://github.com/RhetTbull/osxphotos/blob/main/examples/concurrent_export.py).
#### Contributors
- [@RhetTbull](https://github.com/RhetTbull) for code changes.
- [@eecue](https://github.com/eecue) for testing and helping pinpoint the issues.
## [v0.58.2](https://github.com/RhetTbull/osxphotos/compare/v0.58.1...v0.58.2)
### 14 March 2023
#### Changed
- batch-edit no longer overwrites keywords but instead merges new keywords with existing keywords
#### Added
- added --replace-keywords flag to `osxphotos batch-edit` to force replacement of keywords
#### Contributors
- [@RhetTbull](https://github.com/RhetTbull) for code changes.
- [@pekingduck](https://github.com/pekingduck) for pointing out the deisgn flaw in `batch-edit --keywords` logic
## [v0.58.1](https://github.com/RhetTbull/osxphotos/compare/v0.58.0...v0.58.1)
### 09 March 2023

View File

@@ -2094,7 +2094,7 @@ Substitution Description
{cr} A carriage return: '\r'
{crlf} A carriage return + line feed: '\r\n'
{tab} :A tab: '\t'
{osxphotos_version} The osxphotos version, e.g. '0.58.2'
{osxphotos_version} The osxphotos version, e.g. '0.59.0'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified
@@ -2581,7 +2581,7 @@ The following template field substitutions are availabe for use the templating s
|{cr}|A carriage return: '\r'|
|{crlf}|A carriage return + line feed: '\r\n'|
|{tab}|:A tab: '\t'|
|{osxphotos_version}|The osxphotos version, e.g. '0.58.2'|
|{osxphotos_version}|The osxphotos version, e.g. '0.59.0'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -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: dd34c3ea1cc9003033a129207a3fd912
config: 97939ee226e568d1b630529e47e51a47
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

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

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos._constants - osxphotos 0.58.1 documentation</title>
<title>osxphotos._constants - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -199,10 +199,14 @@
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">sqlite3</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>
<span class="n">logger</span><span class="p">:</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">&quot;osxphotos&quot;</span><span class="p">)</span>
<span class="n">APP_NAME</span> <span class="o">=</span> <span class="s2">&quot;osxphotos&quot;</span>
<span class="n">OSXPHOTOS_URL</span> <span class="o">=</span> <span class="s2">&quot;https://github.com/RhetTbull/osxphotos&quot;</span>
@@ -661,6 +665,15 @@
<span class="n">UUID_PATTERN</span> <span class="o">=</span> <span class="p">(</span>
<span class="sa">r</span><span class="s2">&quot;[0-9a-fA-F]</span><span class="si">{8}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{12}</span><span class="s2">&quot;</span>
<span class="p">)</span>
<span class="c1"># Reference: https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.threadsafety</span>
<span class="c1"># and https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.connect</span>
<span class="c1"># 3: serialized mode; Threads may share the module, connections and cursors</span>
<span class="c1"># 3 is the default in the python.org python 3.11 distribution</span>
<span class="c1"># earlier versions of python.org python 3.x default to 1 which means threads may not share</span>
<span class="c1"># sqlite3 connections and thus PhotoInfo.export() cannot be used in a multithreaded environment</span>
<span class="c1"># pass SQLITE_CHECK_SAME_THREAD to sqlite3.connect() to enable multithreaded access on systems that support it</span>
<span class="n">SQLITE_CHECK_SAME_THREAD</span> <span class="o">=</span> <span class="ow">not</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">threadsafety</span> <span class="o">==</span> <span class="mi">3</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">SQLITE_CHECK_SAME_THREAD</span><span class="si">=}</span><span class="s2">, </span><span class="si">{</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">threadsafety</span><span class="si">=}</span><span class="s2">&quot;</span><span class="p">)</span>
</pre></div>
</article>
</div>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos.albuminfo - osxphotos 0.58.1 documentation</title>
<title>osxphotos.albuminfo - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -215,6 +215,7 @@
<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_VERSION</span><span class="p">,</span>
<span class="n">TIME_DELTA</span><span class="p">,</span>
<span class="n">AlbumSortOrder</span><span class="p">,</span>
<span class="p">)</span>
@@ -258,7 +259,7 @@
<span class="sd"> including folders, photos, etc.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;title&quot;</span><span class="p">]</span>
@@ -318,7 +319,8 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">end_date</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;For Albums, return end date (most recent image) of album or None for albums with no images</span>
<span class="sd"> For Import Sessions, return end date of import sessions (when import was completed)&quot;&quot;&quot;</span>
<span class="sd"> For Import Sessions, return end date of import sessions (when import was completed)</span>
<span class="sd"> &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">_end_date</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -360,6 +362,17 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return album info as a dict&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;creation_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">creation_date</span><span class="p">,</span>
<span class="s2">&quot;start_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">start_date</span><span class="p">,</span>
<span class="s2">&quot;end_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">end_date</span><span class="p">,</span>
<span class="s2">&quot;owner&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">owner</span><span class="p">,</span>
<span class="s2">&quot;photos&quot;</span><span class="p">:</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">photos</span><span class="p">],</span>
<span class="p">}</span>
<span class="k">def</span> <span class="fm">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return number of photos contained in album&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photos</span><span class="p">)</span>
@@ -371,6 +384,10 @@
<span class="sd"> including folders, photos, etc.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="n">db</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="n">uuid</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;title&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return title / name of album&quot;&quot;&quot;</span>
@@ -402,10 +419,11 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">folder_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of folders the album is contained in</span>
<span class="sd">&quot;&quot;&quot;Return hierarchical list of folders the album is contained in</span>
<span class="sd"> the folder list is in form:</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;, ...]</span>
<span class="sd"> returns empty list if album is not in any folders&quot;&quot;&quot;</span>
<span class="sd"> or empty list if album is not in any folders</span>
<span class="sd"> &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">_folder_names</span>
@@ -415,10 +433,9 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">folder_list</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of folders the album is contained in</span>
<span class="sd"> as list of FolderInfo objects in form</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;, ...]</span>
<span class="sd"> returns empty list if album is not in any folders&quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;Returns list of FolderInfo objects for each folder the album is contained in</span>
<span class="sd"> or empty list if album is not in any folders</span>
<span class="sd"> &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">_folders</span>
@@ -443,7 +460,7 @@
<span class="n">parent_pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">][</span><span class="s2">&quot;parentfolder&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_parent</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">FolderInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbums_pk</span><span class="p">[</span><span class="n">parent_pk</span><span class="p">])</span>
<span class="k">if</span> <span class="n">parent_pk</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_folder_root_pk</span>
<span class="k">if</span> <span class="n">parent_pk</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">parent_pk</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_folder_root_pk</span>
<span class="k">else</span> <span class="kc">None</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_parent</span>
@@ -476,29 +493,82 @@
<span class="k">return</span> <span class="n">index</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Photo with uuid </span><span class="si">{</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> does not appear to be in this album&quot;</span>
<span class="p">)</span></div></div>
<span class="p">)</span></div>
<div class="viewcode-block" id="AlbumInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.AlbumInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return album info as a dict&quot;&quot;&quot;</span>
<span class="n">dict_data</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span>
<span class="n">dict_data</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="n">dict_data</span><span class="p">[</span><span class="s2">&quot;folder_names&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">folder_names</span>
<span class="n">dict_data</span><span class="p">[</span><span class="s2">&quot;folder_list&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">f</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">folder_list</span><span class="p">]</span>
<span class="n">dict_data</span><span class="p">[</span><span class="s2">&quot;sort_order&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">sort_order</span>
<span class="n">dict_data</span><span class="p">[</span><span class="s2">&quot;parent&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">uuid</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">parent</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">dict_data</span></div></div>
<div class="viewcode-block" id="ImportInfo"><a class="viewcode-back" href="../../reference.html#osxphotos.ImportInfo">[docs]</a><span class="k">class</span> <span class="nc">ImportInfo</span><span class="p">(</span><span class="n">AlbumInfoBaseClass</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Information about import sessions&quot;&quot;&quot;</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&gt;=</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="n">db</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="n">uuid</span><span class="p">)</span>
<span class="n">import_session</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_import_group</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</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">_creation_date_timestamp</span> <span class="o">=</span> <span class="n">import_session</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_creation_date_timestamp</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">(</span><span class="mi">1970</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_start_date_timestamp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_creation_date_timestamp</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_end_date_timestamp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_creation_date_timestamp</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_title</span> <span class="o">=</span> <span class="n">import_session</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">_local_tz</span> <span class="o">=</span> <span class="n">get_local_tz</span><span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_creation_date_timestamp</span> <span class="o">+</span> <span class="n">TIME_DELTA</span><span class="p">)</span>
<span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return title / name of import session&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_title</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return list of photos contained in import session&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">_photos</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">uuid_list</span><span class="p">,</span> <span class="n">sort_order</span> <span class="o">=</span> <span class="nb">zip</span><span class="p">(</span>
<span class="o">*</span><span class="p">[</span>
<span class="p">(</span><span class="n">uuid</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;fok_import_session&quot;</span><span class="p">])</span>
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;import_uuid&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&gt;=</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
<span class="n">uuid_list</span><span class="p">,</span> <span class="n">sort_order</span> <span class="o">=</span> <span class="nb">zip</span><span class="p">(</span>
<span class="o">*</span><span class="p">[</span>
<span class="p">(</span><span class="n">uuid</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;fok_import_session&quot;</span><span class="p">])</span>
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;import_uuid&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="n">sorted_uuid</span> <span class="o">=</span> <span class="n">sort_list_by_keys</span><span class="p">(</span><span class="n">uuid_list</span><span class="p">,</span> <span class="n">sort_order</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_by_uuid</span><span class="p">(</span><span class="n">sorted_uuid</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">import_photo_uuids</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">u</span>
<span class="k">for</span> <span class="n">u</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">u</span><span class="p">][</span><span class="s2">&quot;import_uuid&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="n">sorted_uuid</span> <span class="o">=</span> <span class="n">sort_list_by_keys</span><span class="p">(</span><span class="n">uuid_list</span><span class="p">,</span> <span class="n">sort_order</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_by_uuid</span><span class="p">(</span><span class="n">sorted_uuid</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_by_uuid</span><span class="p">(</span><span class="n">import_photo_uuids</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photos</span>
<div class="viewcode-block" id="ImportInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.ImportInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return import info as a dict&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;creation_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">creation_date</span><span class="p">,</span>
<span class="s2">&quot;start_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">start_date</span><span class="p">,</span>
<span class="s2">&quot;end_date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">end_date</span><span class="p">,</span>
<span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="s2">&quot;photos&quot;</span><span class="p">:</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">photos</span><span class="p">],</span>
<span class="p">}</span></div>
<span class="k">def</span> <span class="fm">__bool__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Always returns True</span>
<span class="sd"> A photo without an import session will return None for import_info,</span>
@@ -506,6 +576,7 @@
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="kc">True</span></div>
<div class="viewcode-block" id="ProjectInfo"><a class="viewcode-back" href="../../reference.html#osxphotos.ProjectInfo">[docs]</a><span class="k">class</span> <span class="nc">ProjectInfo</span><span class="p">(</span><span class="n">AlbumInfo</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> ProjectInfo with info about projects</span>
@@ -583,7 +654,7 @@
<span class="n">parent_pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">][</span><span class="s2">&quot;parentfolder&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_parent</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">FolderInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbalbums_pk</span><span class="p">[</span><span class="n">parent_pk</span><span class="p">])</span>
<span class="k">if</span> <span class="n">parent_pk</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_folder_root_pk</span>
<span class="k">if</span> <span class="n">parent_pk</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">parent_pk</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_folder_root_pk</span>
<span class="k">else</span> <span class="kc">None</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_parent</span>
@@ -613,6 +684,16 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_folders</span> <span class="o">=</span> <span class="n">folders</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_folders</span>
<div class="viewcode-block" id="FolderInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.FolderInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return folder info as a dict&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;parent&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">uuid</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">parent</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="kc">None</span><span class="p">,</span>
<span class="s2">&quot;subfolders&quot;</span><span class="p">:</span> <span class="p">[</span><span class="n">f</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">subfolders</span><span class="p">],</span>
<span class="s2">&quot;albums&quot;</span><span class="p">:</span> <span class="p">[</span><span class="n">a</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</span><span class="p">],</span>
<span class="p">}</span></div>
<span class="k">def</span> <span class="fm">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns count of folders + albums contained in the folder&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">subfolders</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">album_info</span><span class="p">)</span></div>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos.photoexporter - osxphotos 0.58.1 documentation</title>
<title>osxphotos.photoexporter - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -209,6 +209,7 @@
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">asdict</span><span class="p">,</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>
<span class="kn">from</span> <span class="nn">types</span> <span class="kn">import</span> <span class="n">SimpleNamespace</span>
<span class="kn">import</span> <span class="nn">photoscript</span>
<span class="kn">from</span> <span class="nn">mako.template</span> <span class="kn">import</span> <span class="n">Template</span>
@@ -228,7 +229,7 @@
<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">.datetime_utils</span> <span class="kn">import</span> <span class="n">datetime_tz_to_utc</span>
<span class="kn">from</span> <span class="nn">.exiftool</span> <span class="kn">import</span> <span class="n">ExifTool</span><span class="p">,</span> <span class="n">exiftool_can_write</span>
<span class="kn">from</span> <span class="nn">.exiftool</span> <span class="kn">import</span> <span class="n">ExifTool</span><span class="p">,</span> <span class="n">ExifToolCaching</span><span class="p">,</span> <span class="n">exiftool_can_write</span><span class="p">,</span> <span class="n">get_exiftool_path</span>
<span class="kn">from</span> <span class="nn">.export_db</span> <span class="kn">import</span> <span class="n">ExportDB</span><span class="p">,</span> <span class="n">ExportDBTemp</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">.photokit</span> <span class="kn">import</span> <span class="p">(</span>
@@ -265,6 +266,7 @@
<span class="c1"># retry if download_missing/use_photos_export fails the first time (which sometimes it does)</span>
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
<span class="c1"># return values for _should_update_photo</span>
<span class="k">class</span> <span class="nc">ShouldUpdate</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
<span class="n">NOT_IN_DATABASE</span> <span class="o">=</span> <span class="mi">1</span>
@@ -506,7 +508,6 @@
<span class="n">xattr_skipped</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">xattr_written</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">):</span>
<span class="n">local_vars</span> <span class="o">=</span> <span class="nb">locals</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_datetime</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="k">for</span> <span class="n">attr</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">attributes</span><span class="p">:</span>
@@ -571,7 +572,7 @@
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">photo</span><span class="p">:</span> <span class="s2">&quot;PhotoInfo&quot;</span><span class="p">,</span> <span class="n">tmpdir</span><span class="p">:</span> <span class="n">t</span><span class="o">.</span><span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">photo</span> <span class="o">=</span> <span class="n">photo</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_render_options</span> <span class="o">=</span> <span class="n">RenderOptions</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">_verbose</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">_verbose</span>
<span class="c1"># define functions for adding markup</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_filepath</span> <span class="o">=</span> <span class="n">add_rich_markup_tag</span><span class="p">(</span><span class="s2">&quot;filepath&quot;</span><span class="p">,</span> <span class="n">rich</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
@@ -1147,7 +1148,8 @@
<span class="sd">&quot;&quot;&quot;Stage a photo for export with AppleScript to a temporary directory</span>
<span class="sd"> Note: If exporting an edited live photo, the associated live video will not be exported.</span>
<span class="sd"> This is a limitation of the Photos AppleScript interface and Photos behaves the same way.&quot;&quot;&quot;</span>
<span class="sd"> This is a limitation of the Photos AppleScript interface and Photos behaves the same way.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">edited</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&quot;Edited version requested but photo has no adjustments&quot;</span><span class="p">)</span>
@@ -1761,7 +1763,7 @@
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span>
<span class="n">filepath</span><span class="p">,</span>
<span class="n">flags</span><span class="o">=</span><span class="n">options</span><span class="o">.</span><span class="n">exiftool_flags</span><span class="p">,</span>
<span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">,</span>
<span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">,</span>
<span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
<span class="k">for</span> <span class="n">exiftag</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif_info</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
@@ -1941,7 +1943,6 @@
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">ismovie</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;Keys:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;UserData:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="c1"># process date/time and timezone offset</span>
<span class="c1"># Photos exports the following fields and sets modify date to creation date</span>
<span class="c1"># [EXIF] Modify Date : 2020:10:30 00:00:00</span>
@@ -2051,7 +2052,7 @@
<span class="k">def</span> <span class="nf">_get_exif_keywords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns list of keywords found in the file&#39;s exif metadata&quot;&quot;&quot;</span>
<span class="n">keywords</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">exif</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">exiftool</span>
<span class="n">exif</span> <span class="o">=</span> <span class="n">exiftool_caching</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exif</span><span class="p">:</span>
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exif</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span>
<span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&quot;IPTC:Keywords&quot;</span><span class="p">,</span> <span class="s2">&quot;XMP:TagsList&quot;</span><span class="p">,</span> <span class="s2">&quot;XMP:Subject&quot;</span><span class="p">]:</span>
@@ -2068,7 +2069,7 @@
<span class="k">def</span> <span class="nf">_get_exif_persons</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns list of persons found in the file&#39;s exif metadata&quot;&quot;&quot;</span>
<span class="n">persons</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">exif</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">exiftool</span>
<span class="n">exif</span> <span class="o">=</span> <span class="n">exiftool_caching</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exif</span><span class="p">:</span>
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exif</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
@@ -2339,6 +2340,35 @@
<span class="k">else</span><span class="p">:</span>
<span class="n">new_files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
<span class="k">return</span> <span class="n">new_files</span>
<span class="k">def</span> <span class="nf">exiftool_caching</span><span class="p">(</span><span class="n">photo</span><span class="p">:</span> <span class="n">SimpleNamespace</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ExifToolCaching</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;Return ExifToolCaching object for photo</span>
<span class="sd"> Args:</span>
<span class="sd"> photo: SimpleNamespace object with photo info</span>
<span class="sd"> Returns:</span>
<span class="sd"> ExifToolCaching object</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="n">photo</span><span class="o">.</span><span class="n">_exiftool_caching</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">exiftool_path</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">_exiftool_path</span> <span class="ow">or</span> <span class="n">get_exiftool_path</span><span class="p">()</span>
<span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">path</span><span class="p">):</span>
<span class="n">exiftool</span> <span class="o">=</span> <span class="n">ExifToolCaching</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="n">exiftool_path</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">exiftool</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">except</span> <span class="ne">FileNotFoundError</span><span class="p">:</span>
<span class="c1"># get_exiftool_path raises FileNotFoundError if exiftool not found</span>
<span class="n">exiftool</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
<span class="s2">&quot;exiftool not in path; download and install from https://exiftool.org/&quot;</span>
<span class="p">)</span>
<span class="n">photo</span><span class="o">.</span><span class="n">_exiftool_caching</span> <span class="o">=</span> <span class="n">exiftool</span>
<span class="k">return</span> <span class="n">photo</span><span class="o">.</span><span class="n">_exiftool_caching</span>
</pre></div>
</article>
</div>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos.photoinfo - osxphotos 0.58.1 documentation</title>
<title>osxphotos.photoinfo - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -205,14 +205,16 @@
<span class="kn">import</span> <span class="nn">dataclasses</span>
<span class="kn">import</span> <span class="nn">datetime</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">plistlib</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cached_property</span>
<span class="kn">from</span> <span class="nn">types</span> <span class="kn">import</span> <span class="n">SimpleNamespace</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Optional</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">yaml</span>
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
@@ -263,7 +265,7 @@
<span class="kn">from</span> <span class="nn">.uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">hexdigest</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;PhotoInfo&quot;</span><span class="p">,</span> <span class="s2">&quot;PhotoInfoNone&quot;</span><span class="p">]</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;PhotoInfo&quot;</span><span class="p">,</span> <span class="s2">&quot;PhotoInfoNone&quot;</span><span class="p">,</span> <span class="s2">&quot;frozen_photoinfo_factory&quot;</span><span class="p">]</span>
<span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">&quot;osxphotos&quot;</span><span class="p">)</span>
@@ -278,6 +280,7 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">uuid</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span> <span class="o">=</span> <span class="n">info</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">:</span> <span class="s2">&quot;osxphotos.PhotosDB&quot;</span> <span class="o">=</span> <span class="n">db</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool_path</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_verbose</span>
<span class="nd">@property</span>
@@ -585,6 +588,8 @@
<span class="sd">&quot;&quot;&quot;return path_edited_live_photo for Photos &lt;= 4&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&gt;</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">&quot;Wrong database format!&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_predicted_path_edited_live_photo_4</span><span class="p">()</span>
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
<span class="c1"># the heuristic failed, so try to find the file</span>
@@ -598,10 +603,6 @@
<span class="p">),</span>
<span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;MISSING PATH: edited live photo file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> does not appear to exist&quot;</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">photopath</span>
<span class="k">def</span> <span class="nf">_path_edited_5_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1395,7 +1396,6 @@
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;score not implemented for this database version&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">try</span><span class="p">:</span>
@@ -1541,7 +1541,6 @@
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;exif_info not implemented for this database version&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">try</span><span class="p">:</span>
@@ -1624,11 +1623,14 @@
<span class="k">def</span> <span class="nf">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns a unique digest of the photo&#39;s properties and metadata;</span>
<span class="sd"> useful for detecting changes in any property/metadata of the photo&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">json</span><span class="p">())</span>
<span class="k">return</span> <span class="n">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_json_hexdigest</span><span class="p">())</span>
<span class="nd">@cached_property</span>
<span class="k">def</span> <span class="nf">cloud_metadata</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict&quot;&quot;&quot;</span>
<span class="k">def</span> <span class="nf">cloud_metadata</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">[</span><span class="n">Any</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
<span class="sd">&quot;&quot;&quot;Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict; Photos 5+ only&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="p">{}</span>
<span class="c1"># This is a large blob of data so don&#39;t load it unless requested</span>
<span class="n">asset_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span><span class="p">][</span><span class="s2">&quot;ASSET&quot;</span><span class="p">]</span>
<span class="n">sql_cloud_metadata</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;&quot;&quot;</span>
@@ -1639,10 +1641,6 @@
<span class="s2"> WHERE </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID = ?</span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;cloud_metadata not implemented for this database version&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="p">{}</span>
<span class="n">_</span><span class="p">,</span> <span class="n">cursor</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
<span class="n">metadata</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">if</span> <span class="n">results</span> <span class="o">:=</span> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql_cloud_metadata</span><span class="p">,</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,))</span><span class="o">.</span><span class="n">fetchone</span><span class="p">():</span>
@@ -1979,80 +1977,112 @@
<div class="viewcode-block" id="PhotoInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return dict representation&quot;&quot;&quot;</span>
<span class="n">folders</span> <span class="o">=</span> <span class="p">{</span><span class="n">album</span><span class="o">.</span><span class="n">title</span><span class="p">:</span> <span class="n">album</span><span class="o">.</span><span class="n">folder_names</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">album_info</span><span class="p">}</span>
<span class="n">exif</span> <span class="o">=</span> <span class="n">dataclasses</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">exif_info</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">exif_info</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">place</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">place</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">place</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">score</span> <span class="o">=</span> <span class="n">dataclasses</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">score</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">score</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">adjustments</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">album_info</span> <span class="o">=</span> <span class="p">[</span><span class="n">album</span><span class="o">.</span><span class="n">asdict</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">album_info</span><span class="p">]</span>
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[</span><span class="n">a</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_album_info</span><span class="p">]</span>
<span class="n">burst_photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">]</span>
<span class="n">comments</span> <span class="o">=</span> <span class="p">[</span><span class="n">comment</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">comment</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">comments</span><span class="p">]</span>
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">dataclasses</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">exif_info</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">exif_info</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">face_info</span> <span class="o">=</span> <span class="p">[</span><span class="n">face</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">face</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_info</span><span class="p">]</span>
<span class="n">folders</span> <span class="o">=</span> <span class="p">{</span><span class="n">album</span><span class="o">.</span><span class="n">title</span><span class="p">:</span> <span class="n">album</span><span class="o">.</span><span class="n">folder_names</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">album_info</span><span class="p">}</span>
<span class="n">import_info</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">import_info</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">import_info</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">likes</span> <span class="o">=</span> <span class="p">[</span><span class="n">like</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">like</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">likes</span><span class="p">]</span>
<span class="n">faces</span> <span class="o">=</span> <span class="p">[</span><span class="n">face</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">face</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_info</span><span class="p">]</span>
<span class="n">person_info</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">person_info</span><span class="p">]</span>
<span class="n">place</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">place</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">place</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">project_info</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">project_info</span><span class="p">]</span>
<span class="n">score</span> <span class="o">=</span> <span class="n">dataclasses</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">score</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">score</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">search_info</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info</span> <span class="k">else</span> <span class="p">{}</span>
<span class="n">search_info_normalized</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">search_info_normalized</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info_normalized</span> <span class="k">else</span> <span class="p">{}</span>
<span class="p">)</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;library&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;filename&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">filename</span><span class="p">,</span>
<span class="s2">&quot;original_filename&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span><span class="p">,</span>
<span class="s2">&quot;adjustments&quot;</span><span class="p">:</span> <span class="n">adjustments</span><span class="p">,</span>
<span class="s2">&quot;album_info&quot;</span><span class="p">:</span> <span class="n">album_info</span><span class="p">,</span>
<span class="s2">&quot;albums&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">,</span>
<span class="s2">&quot;burst_album_info&quot;</span><span class="p">:</span> <span class="n">burst_album_info</span><span class="p">,</span>
<span class="s2">&quot;burst_albums&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_albums</span><span class="p">,</span>
<span class="s2">&quot;burst_default_pick&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_default_pick</span><span class="p">,</span>
<span class="s2">&quot;burst_key&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_key</span><span class="p">,</span>
<span class="s2">&quot;burst_photos&quot;</span><span class="p">:</span> <span class="n">burst_photos</span><span class="p">,</span>
<span class="s2">&quot;burst_selected&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span><span class="p">,</span>
<span class="s2">&quot;burst&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">,</span>
<span class="s2">&quot;cloud_guid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_guid</span><span class="p">,</span>
<span class="s2">&quot;cloud_metadata&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_metadata</span><span class="p">,</span>
<span class="s2">&quot;cloud_owner_hashed_id&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_owner_hashed_id</span><span class="p">,</span>
<span class="s2">&quot;comments&quot;</span><span class="p">:</span> <span class="n">comments</span><span class="p">,</span>
<span class="s2">&quot;date_added&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_added</span><span class="p">,</span>
<span class="s2">&quot;date_modified&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span><span class="p">,</span>
<span class="s2">&quot;date_trashed&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_trashed</span><span class="p">,</span>
<span class="s2">&quot;date&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">date</span><span class="p">,</span>
<span class="s2">&quot;description&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span><span class="p">,</span>
<span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="s2">&quot;keywords&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
<span class="s2">&quot;labels&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">labels</span><span class="p">,</span>
<span class="s2">&quot;keywords&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
<span class="s2">&quot;albums&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">,</span>
<span class="s2">&quot;folders&quot;</span><span class="p">:</span> <span class="n">folders</span><span class="p">,</span>
<span class="s2">&quot;persons&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">,</span>
<span class="s2">&quot;faces&quot;</span><span class="p">:</span> <span class="n">faces</span><span class="p">,</span>
<span class="s2">&quot;path&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
<span class="s2">&quot;ismissing&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">,</span>
<span class="s2">&quot;hasadjustments&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">,</span>
<span class="s2">&quot;exif_info&quot;</span><span class="p">:</span> <span class="n">exif_info</span><span class="p">,</span>
<span class="s2">&quot;external_edit&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">external_edit</span><span class="p">,</span>
<span class="s2">&quot;face_info&quot;</span><span class="p">:</span> <span class="n">face_info</span><span class="p">,</span>
<span class="s2">&quot;favorite&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">favorite</span><span class="p">,</span>
<span class="s2">&quot;filename&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">filename</span><span class="p">,</span>
<span class="s2">&quot;fingerprint&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">fingerprint</span><span class="p">,</span>
<span class="s2">&quot;folders&quot;</span><span class="p">:</span> <span class="n">folders</span><span class="p">,</span>
<span class="s2">&quot;has_raw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_raw</span><span class="p">,</span>
<span class="s2">&quot;hasadjustments&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">,</span>
<span class="s2">&quot;hdr&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hdr</span><span class="p">,</span>
<span class="s2">&quot;height&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span><span class="p">,</span>
<span class="s2">&quot;hidden&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hidden</span><span class="p">,</span>
<span class="s2">&quot;latitude&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_latitude</span><span class="p">,</span>
<span class="s2">&quot;longitude&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_longitude</span><span class="p">,</span>
<span class="s2">&quot;path_edited&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">,</span>
<span class="s2">&quot;shared&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">shared</span><span class="p">,</span>
<span class="s2">&quot;isphoto&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">,</span>
<span class="s2">&quot;ismovie&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">,</span>
<span class="s2">&quot;uti&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span><span class="p">,</span>
<span class="s2">&quot;uti_original&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span><span class="p">,</span>
<span class="s2">&quot;burst&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">,</span>
<span class="s2">&quot;live_photo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span><span class="p">,</span>
<span class="s2">&quot;path_live_photo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_live_photo</span><span class="p">,</span>
<span class="s2">&quot;iscloudasset&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">iscloudasset</span><span class="p">,</span>
<span class="s2">&quot;import_info&quot;</span><span class="p">:</span> <span class="n">import_info</span><span class="p">,</span>
<span class="s2">&quot;incloud&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">incloud</span><span class="p">,</span>
<span class="s2">&quot;intrash&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
<span class="s2">&quot;iscloudasset&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">iscloudasset</span><span class="p">,</span>
<span class="s2">&quot;ismissing&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">,</span>
<span class="s2">&quot;ismovie&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">,</span>
<span class="s2">&quot;isphoto&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">,</span>
<span class="s2">&quot;israw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">israw</span><span class="p">,</span>
<span class="s2">&quot;isreference&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">isreference</span><span class="p">,</span>
<span class="s2">&quot;date_modified&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span><span class="p">,</span>
<span class="s2">&quot;keywords&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
<span class="s2">&quot;labels_normalized&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">labels_normalized</span><span class="p">,</span>
<span class="s2">&quot;labels&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">labels</span><span class="p">,</span>
<span class="s2">&quot;latitude&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_latitude</span><span class="p">,</span>
<span class="s2">&quot;library&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
<span class="s2">&quot;likes&quot;</span><span class="p">:</span> <span class="n">likes</span><span class="p">,</span>
<span class="s2">&quot;live_photo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span><span class="p">,</span>
<span class="s2">&quot;location&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span><span class="p">,</span>
<span class="s2">&quot;longitude&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_longitude</span><span class="p">,</span>
<span class="s2">&quot;orientation&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span><span class="p">,</span>
<span class="s2">&quot;original_filename&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span><span class="p">,</span>
<span class="s2">&quot;original_filesize&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filesize</span><span class="p">,</span>
<span class="s2">&quot;original_height&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_height</span><span class="p">,</span>
<span class="s2">&quot;original_orientation&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_orientation</span><span class="p">,</span>
<span class="s2">&quot;original_width&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_width</span><span class="p">,</span>
<span class="s2">&quot;owner&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">owner</span><span class="p">,</span>
<span class="s2">&quot;panorama&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">panorama</span><span class="p">,</span>
<span class="s2">&quot;path_derivatives&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">,</span>
<span class="s2">&quot;path_edited_live_photo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited_live_photo</span><span class="p">,</span>
<span class="s2">&quot;path_edited&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">,</span>
<span class="s2">&quot;path_live_photo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_live_photo</span><span class="p">,</span>
<span class="s2">&quot;path_raw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span><span class="p">,</span>
<span class="s2">&quot;path&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
<span class="s2">&quot;person_info&quot;</span><span class="p">:</span> <span class="n">person_info</span><span class="p">,</span>
<span class="s2">&quot;persons&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">,</span>
<span class="s2">&quot;place&quot;</span><span class="p">:</span> <span class="n">place</span><span class="p">,</span>
<span class="s2">&quot;portrait&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">portrait</span><span class="p">,</span>
<span class="s2">&quot;project_info&quot;</span><span class="p">:</span> <span class="n">project_info</span><span class="p">,</span>
<span class="s2">&quot;raw_original&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">raw_original</span><span class="p">,</span>
<span class="s2">&quot;score&quot;</span><span class="p">:</span> <span class="n">score</span><span class="p">,</span>
<span class="s2">&quot;screenshot&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">screenshot</span><span class="p">,</span>
<span class="s2">&quot;search_info_normalized&quot;</span><span class="p">:</span> <span class="n">search_info_normalized</span><span class="p">,</span>
<span class="s2">&quot;search_info&quot;</span><span class="p">:</span> <span class="n">search_info</span><span class="p">,</span>
<span class="s2">&quot;selfie&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">selfie</span><span class="p">,</span>
<span class="s2">&quot;shared&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">shared</span><span class="p">,</span>
<span class="s2">&quot;slow_mo&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">slow_mo</span><span class="p">,</span>
<span class="s2">&quot;time_lapse&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">time_lapse</span><span class="p">,</span>
<span class="s2">&quot;hdr&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hdr</span><span class="p">,</span>
<span class="s2">&quot;selfie&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">selfie</span><span class="p">,</span>
<span class="s2">&quot;panorama&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">panorama</span><span class="p">,</span>
<span class="s2">&quot;has_raw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_raw</span><span class="p">,</span>
<span class="s2">&quot;israw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">israw</span><span class="p">,</span>
<span class="s2">&quot;raw_original&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">raw_original</span><span class="p">,</span>
<span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
<span class="s2">&quot;tzoffset&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">tzoffset</span><span class="p">,</span>
<span class="s2">&quot;uti_edited&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_edited</span><span class="p">,</span>
<span class="s2">&quot;uti_original&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span><span class="p">,</span>
<span class="s2">&quot;uti_raw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_raw</span><span class="p">,</span>
<span class="s2">&quot;path_raw&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span><span class="p">,</span>
<span class="s2">&quot;place&quot;</span><span class="p">:</span> <span class="n">place</span><span class="p">,</span>
<span class="s2">&quot;exif&quot;</span><span class="p">:</span> <span class="n">exif</span><span class="p">,</span>
<span class="s2">&quot;score&quot;</span><span class="p">:</span> <span class="n">score</span><span class="p">,</span>
<span class="s2">&quot;intrash&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
<span class="s2">&quot;height&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span><span class="p">,</span>
<span class="s2">&quot;uti&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;visible&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">visible</span><span class="p">,</span>
<span class="s2">&quot;width&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">width</span><span class="p">,</span>
<span class="s2">&quot;orientation&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span><span class="p">,</span>
<span class="s2">&quot;original_height&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_height</span><span class="p">,</span>
<span class="s2">&quot;original_width&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_width</span><span class="p">,</span>
<span class="s2">&quot;original_orientation&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_orientation</span><span class="p">,</span>
<span class="s2">&quot;original_filesize&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filesize</span><span class="p">,</span>
<span class="s2">&quot;comments&quot;</span><span class="p">:</span> <span class="n">comments</span><span class="p">,</span>
<span class="s2">&quot;likes&quot;</span><span class="p">:</span> <span class="n">likes</span><span class="p">,</span>
<span class="s2">&quot;search_info&quot;</span><span class="p">:</span> <span class="n">search_info</span><span class="p">,</span>
<span class="s2">&quot;fingerprint&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">fingerprint</span><span class="p">,</span>
<span class="s2">&quot;cloud_guid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_guid</span><span class="p">,</span>
<span class="s2">&quot;cloud_owner_hashed_id&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_owner_hashed_id</span><span class="p">,</span>
<span class="p">}</span></div>
<div class="viewcode-block" id="PhotoInfo.json"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -2064,10 +2094,46 @@
<span class="n">dict_data</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span>
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dict_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="c1"># sort lists such as keywords so JSON is consistent</span>
<span class="c1"># but do not sort certain items like location</span>
<span class="k">if</span> <span class="n">k</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&quot;location&quot;</span><span class="p">]:</span>
<span class="k">continue</span>
<span class="k">if</span> <span class="n">v</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">))</span> <span class="ow">and</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nb">dict</span><span class="p">):</span>
<span class="n">dict_data</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
<span class="n">dict_data</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">v</span><span class="p">:</span> <span class="n">v</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">dict_data</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">default</span><span class="p">)</span></div>
<span class="k">def</span> <span class="nf">_json_hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;JSON for use by hexdigest()&quot;&quot;&quot;</span>
<span class="c1"># This differs from json() because hexdigest must not change if metadata changed</span>
<span class="c1"># With json(), sort order of lists of dicts is not consistent but these aren&#39;t needed</span>
<span class="c1"># for computing hexdigest so we can ignore them</span>
<span class="c1"># also don&#39;t use visible because it changes based on Photos UI state</span>
<span class="k">def</span> <span class="nf">default</span><span class="p">(</span><span class="n">o</span><span class="p">):</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">o</span><span class="p">,</span> <span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">date</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="p">)):</span>
<span class="k">return</span> <span class="n">o</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">dict_data</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span>
<span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="p">[</span>
<span class="s2">&quot;album_info&quot;</span><span class="p">,</span>
<span class="s2">&quot;burst_album_info&quot;</span><span class="p">,</span>
<span class="s2">&quot;face_info&quot;</span><span class="p">,</span>
<span class="s2">&quot;person_info&quot;</span><span class="p">,</span>
<span class="s2">&quot;visible&quot;</span><span class="p">,</span>
<span class="p">]:</span>
<span class="k">del</span> <span class="n">dict_data</span><span class="p">[</span><span class="n">k</span><span class="p">]</span>
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dict_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="c1"># sort lists such as keywords so JSON is consistent</span>
<span class="c1"># but do not sort certain items like location</span>
<span class="k">if</span> <span class="n">k</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&quot;location&quot;</span><span class="p">]:</span>
<span class="k">continue</span>
<span class="k">if</span> <span class="n">v</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">))</span> <span class="ow">and</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nb">dict</span><span class="p">):</span>
<span class="n">dict_data</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">v</span><span class="p">:</span> <span class="n">v</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">dict_data</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">default</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Compare two PhotoInfo objects for equality&quot;&quot;&quot;</span>
<span class="c1"># Can&#39;t just compare the two __dicts__ because some methods (like albums)</span>
@@ -2090,13 +2156,114 @@
<span class="k">class</span> <span class="nc">PhotoInfoNone</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;mock class that returns None for all attributes&quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;Mock class that returns None for all attributes&quot;&quot;&quot;</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">def</span> <span class="fm">__getattribute__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">def</span> <span class="nf">frozen_photoinfo_factory</span><span class="p">(</span><span class="n">photo</span><span class="p">:</span> <span class="n">PhotoInfo</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">SimpleNamespace</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;Return a frozen SimpleNamespace object for a PhotoInfo object&quot;&quot;&quot;</span>
<span class="n">photo_json</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_object_hook</span><span class="p">(</span><span class="n">d</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="n">Any</span><span class="p">,</span> <span class="n">Any</span><span class="p">]):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">d</span><span class="p">:</span>
<span class="k">return</span> <span class="n">d</span>
<span class="c1"># if d key matches a ISO 8601 datetime (&#39;2023-03-24T06:46:57.690786&#39;, &#39;2019-07-04T16:24:01-07:00&#39;, &#39;2019-07-04T16:24:01+07:00&#39;), convert to datetime</span>
<span class="c1"># fromisoformat will also handle dates with timezone offset in form +0700, etc.</span>
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">d</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="ow">and</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span>
<span class="sa">r</span><span class="s2">&quot;\d</span><span class="si">{4}</span><span class="s2">-\d</span><span class="si">{2}</span><span class="s2">-\d</span><span class="si">{2}</span><span class="s2">T\d</span><span class="si">{2}</span><span class="s2">:\d</span><span class="si">{2}</span><span class="s2">:\d</span><span class="si">{2}</span><span class="s2">[.]?\d*[+-]?\d</span><span class="si">{2}</span><span class="s2">[:]?\d</span><span class="si">{2}</span><span class="s2">?&quot;</span><span class="p">,</span> <span class="n">v</span>
<span class="p">):</span>
<span class="n">d</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
<span class="k">return</span> <span class="n">SimpleNamespace</span><span class="p">(</span><span class="o">**</span><span class="n">d</span><span class="p">)</span>
<span class="n">frozen</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">photo_json</span><span class="p">,</span> <span class="n">object_hook</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">_object_hook</span><span class="p">(</span><span class="n">d</span><span class="p">))</span>
<span class="c1"># add on json() method to frozen object</span>
<span class="k">def</span> <span class="nf">_json</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">):</span>
<span class="k">return</span> <span class="n">photo_json</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">json</span> <span class="o">=</span> <span class="n">_json</span>
<span class="c1"># add hexdigest property to frozen object</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">hexdigest</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">hexdigest</span>
<span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Detects text in photo and returns lists of results as (detected text, confidence)</span>
<span class="sd"> confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,</span>
<span class="sd"> text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD</span>
<span class="sd"> If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available</span>
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text_cache</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">()</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="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;Error detecting text in photo </span><span class="si">{</span><span class="n">frozen</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">_detected_text</span><span class="p">():</span>
<span class="sd">&quot;&quot;&quot;detect text in photo, either from cached extended attribute or by attempting text detection&quot;&quot;&quot;</span>
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">path_edited</span>
<span class="k">if</span> <span class="n">frozen</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="n">frozen</span><span class="o">.</span><span class="n">path_edited</span>
<span class="k">else</span> <span class="n">frozen</span><span class="o">.</span><span class="n">path</span>
<span class="p">)</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">path</span> <span class="ow">or</span> <span class="n">frozen</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">frozen</span><span class="o">.</span><span class="n">path_derivatives</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="n">md</span> <span class="o">=</span> <span class="n">OSXMetaData</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">decoder</span><span class="p">(</span><span class="n">val</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Decode value from JSON&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">val</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&quot;utf-8&quot;</span><span class="p">))</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">get_xattr</span><span class="p">(</span>
<span class="s2">&quot;osxphotos.metadata:detected_text&quot;</span><span class="p">,</span> <span class="n">decode</span><span class="o">=</span><span class="n">decoder</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">detected_text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">orientation</span> <span class="o">=</span> <span class="n">frozen</span><span class="o">.</span><span class="n">orientation</span> <span class="ow">or</span> <span class="kc">None</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">orientation</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">encoder</span><span class="p">(</span><span class="n">obj</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Encode value as JSON&quot;&quot;&quot;</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
<span class="k">return</span> <span class="n">val</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">&quot;utf-8&quot;</span><span class="p">)</span>
<span class="n">md</span><span class="o">.</span><span class="n">set_xattr</span><span class="p">(</span>
<span class="s2">&quot;osxphotos.metadata:detected_text&quot;</span><span class="p">,</span> <span class="n">detected_text</span><span class="p">,</span> <span class="n">encode</span><span class="o">=</span><span class="n">encoder</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">detected_text</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">detected_text</span> <span class="o">=</span> <span class="n">detected_text</span>
<span class="n">frozen</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="n">_detected_text</span>
<span class="k">return</span> <span class="n">frozen</span>
</pre></div>
</article>
</div>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../../genindex.html" /><link rel="search" title="Search" href="../../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos.photosdb.photosdb - osxphotos 0.58.1 documentation</title>
<title>osxphotos.photosdb.photosdb - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -481,6 +481,9 @@
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># Dict to hold data on imports for Photos &lt;= 4</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_import_group</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dbfile</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos.phototemplate - osxphotos 0.58.1 documentation</title>
<title>osxphotos.phototemplate - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.58.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.58.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -1549,7 +1549,7 @@
<span class="n">subfield</span> <span class="o">=</span> <span class="n">subfield</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">if</span> <span class="n">subfield</span> <span class="ow">in</span> <span class="n">exifdict</span><span class="p">:</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">exifdict</span><span class="p">[</span><span class="n">subfield</span><span class="p">]</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="p">]</span> <span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="nb">list</span><span class="p">)</span> <span class="k">else</span> <span class="n">values</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">values</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="nb">list</span><span class="p">)</span> <span class="k">else</span> <span class="p">[</span><span class="n">values</span><span class="p">]</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="c1"># sanitize directory names if needed</span>

View File

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

View File

@@ -361,7 +361,7 @@ Template Substitutions
* - {tab}
- :A tab: '\t'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.58.2'
- The osxphotos version, e.g. '0.59.0'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}

View File

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

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Template System" href="template_help.html" /><link rel="prev" title="OSXPhotos Tutorial" href="tutorial.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.58.2 documentation</title>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.58.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.58.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -812,7 +812,7 @@ Operates on currently selected photos.</p>
</div>
<p>This will set the title to “California vacation 2023 2023-02-20 001”, and so on,
the description to the reverse geolocation place name,
and add the keywords to “Family”, “Travel”.</p>
and add the keywords “Family” and “Travel”.</p>
<p>title, description, and keyword may be any valid template string.
See <a class="reference external" href="https://rhettbull.github.io/osxphotos/template_help.html">https://rhettbull.github.io/osxphotos/template_help.html</a>
or <cite>osxphotos docs</cite> for more information on the osxphotos template system.</p>

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Index - osxphotos 0.58.2 documentation</title>
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Index - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -122,7 +122,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.58.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -145,7 +145,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.58.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -2991,12 +2991,12 @@
<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>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<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.AlbumSortOrder">AlbumSortOrder (class in osxphotos)</a>
@@ -3012,9 +3012,15 @@
<li><a href="cli.html#cmdoption-osxphotos-run-arg-ARGS">osxphotos-run command line option</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.ExifTool.asdict">asdict() (osxphotos.ExifTool method)</a>
<li><a href="reference.html#osxphotos.AlbumInfo.asdict">asdict() (osxphotos.AlbumInfo method)</a>
<ul>
<li><a href="reference.html#osxphotos.ExifTool.asdict">(osxphotos.ExifTool method)</a>
</li>
<li><a href="reference.html#osxphotos.FolderInfo.asdict">(osxphotos.FolderInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.ImportInfo.asdict">(osxphotos.ImportInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.MomentInfo.asdict">(osxphotos.MomentInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PersonInfo.asdict">(osxphotos.PersonInfo method)</a>
@@ -3086,11 +3092,13 @@
</li>
<li><a href="reference.html#osxphotos.QueryOptions.cloudasset">cloudasset (osxphotos.QueryOptions attribute)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.CommentInfo">CommentInfo (class in osxphotos)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.comments">comments (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.connection">connection (osxphotos.ExportDB property)</a>
</li>
<li><a href="reference.html#osxphotos.ExportOptions.convert_to_jpeg">convert_to_jpeg (osxphotos.ExportOptions attribute)</a>
</li>
@@ -5709,6 +5717,8 @@
<ul>
<li><a href="reference.html#osxphotos.FolderInfo.title">(osxphotos.FolderInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.ImportInfo.title">(osxphotos.ImportInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.MomentInfo.title">(osxphotos.MomentInfo property)</a>
</li>

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos" href="overview.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>osxphotos 0.58.2 documentation</title>
<title>osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">osxphotos 0.58.2 documentation</div></a>
<a href="#"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
<span class="sidebar-brand-text">osxphotos 0.58.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -284,6 +284,7 @@
</li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a><ul>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo"><code class="docutils literal notranslate"><span class="pre">AlbumInfo</span></code></a><ul>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.asdict"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.asdict()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.folder_list"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_list</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.folder_names"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_names</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.parent"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.parent</span></code></a></li>
@@ -308,6 +309,7 @@
</li>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ExportDB"><code class="docutils literal notranslate"><span class="pre">ExportDB</span></code></a><ul>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportDB.close"><code class="docutils literal notranslate"><span class="pre">ExportDB.close()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportDB.connection"><code class="docutils literal notranslate"><span class="pre">ExportDB.connection</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportDB.create_file_record"><code class="docutils literal notranslate"><span class="pre">ExportDB.create_file_record()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportDB.create_or_get_file_record"><code class="docutils literal notranslate"><span class="pre">ExportDB.create_or_get_file_record()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportDB.delete_data_for_filepath"><code class="docutils literal notranslate"><span class="pre">ExportDB.delete_data_for_filepath()</span></code></a></li>
@@ -397,6 +399,7 @@
</li>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.FolderInfo"><code class="docutils literal notranslate"><span class="pre">FolderInfo</span></code></a><ul>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.FolderInfo.album_info"><code class="docutils literal notranslate"><span class="pre">FolderInfo.album_info</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.FolderInfo.asdict"><code class="docutils literal notranslate"><span class="pre">FolderInfo.asdict()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.FolderInfo.parent"><code class="docutils literal notranslate"><span class="pre">FolderInfo.parent</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.FolderInfo.subfolders"><code class="docutils literal notranslate"><span class="pre">FolderInfo.subfolders</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.FolderInfo.title"><code class="docutils literal notranslate"><span class="pre">FolderInfo.title</span></code></a></li>
@@ -404,7 +407,9 @@
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ImportInfo"><code class="docutils literal notranslate"><span class="pre">ImportInfo</span></code></a><ul>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ImportInfo.asdict"><code class="docutils literal notranslate"><span class="pre">ImportInfo.asdict()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ImportInfo.photos"><code class="docutils literal notranslate"><span class="pre">ImportInfo.photos</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ImportInfo.title"><code class="docutils literal notranslate"><span class="pre">ImportInfo.title</span></code></a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.LikeInfo"><code class="docutils literal notranslate"><span class="pre">LikeInfo</span></code></a></li>

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="prev" title="OSXPhotos Python Package Overview" href="package_overview.html" />
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
<title>OSXPhotos Python Reference - osxphotos 0.58.2 documentation</title>
<title>OSXPhotos Python Reference - osxphotos 0.59.0 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.58.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.59.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.58.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.59.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -201,25 +201,29 @@
<p>__init__.py for osxphotos</p>
<dl class="py class">
<dt class="sig sig-object py" id="osxphotos.AlbumInfo">
<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">AlbumInfo</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><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/albuminfo.html#AlbumInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.AlbumInfo" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">AlbumInfo</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">db</span></span></em>, <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/albuminfo.html#AlbumInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.AlbumInfo" title="Permalink to this definition">#</a></dt>
<dd><p>Info about a specific Album, contains all the details about the album
including folders, photos, etc.</p>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.AlbumInfo.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/albuminfo.html#AlbumInfo.asdict"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.AlbumInfo.asdict" title="Permalink to this definition">#</a></dt>
<dd><p>Return album info as a dict</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.AlbumInfo.folder_list">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">folder_list</span></span><a class="headerlink" href="#osxphotos.AlbumInfo.folder_list" title="Permalink to this definition">#</a></dt>
<dd><p>return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
[“Top level folder”, “sub folder 1”, “sub folder 2”, …]
returns empty list if album is not in any folders</p>
<dd><p>Returns list of FolderInfo objects for each folder the album is contained in
or empty list if album is not in any folders</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.AlbumInfo.folder_names">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">folder_names</span></span><a class="headerlink" href="#osxphotos.AlbumInfo.folder_names" title="Permalink to this definition">#</a></dt>
<dd><p>return hierarchical list of folders the album is contained in
<dd><p>Return hierarchical list of folders the album is contained in
the folder list is in form:
[“Top level folder”, “sub folder 1”, “sub folder 2”, …]
returns empty list if album is not in any folders</p>
or empty list if album is not in any folders</p>
</dd></dl>
<dl class="py property">
@@ -390,7 +394,7 @@ If called in context manager, returns True (execution is delayed until exiting c
<dl class="py class">
<dt class="sig sig-object py" id="osxphotos.ExportDB">
<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">ExportDB</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">dbfile</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">export_dir</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">ExportDB</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">dbfile</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">export_dir</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB" title="Permalink to this definition">#</a></dt>
<dd><p>Interface to sqlite3 database used to store state information for osxphotos export command</p>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.close">
@@ -398,41 +402,47 @@ If called in context manager, returns True (execution is delayed until exiting c
<dd><p>close the database connection</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.ExportDB.connection">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">connection</span></span><em class="property"><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="pre">Connection</span></em><a class="headerlink" href="#osxphotos.ExportDB.connection" title="Permalink to this definition">#</a></dt>
<dd><p>returns sqlite3 connection</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.create_file_record">
<span class="sig-name descname"><span class="pre">create_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Union</span><span class="p"><span class="pre">[</span></span><span class="pre">Path</span><span class="p"><span class="pre">,</span></span><span class="w"> </span><span class="pre">str</span><span class="p"><span class="pre">]</span></span></span></em>, <em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">ExportRecord</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.create_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.create_file_record" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">create_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">ExportRecord</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.create_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.create_file_record" title="Permalink to this definition">#</a></dt>
<dd><p>create a new record for filename and uuid</p>
<p>Returns: an ExportRecord object</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.create_or_get_file_record">
<span class="sig-name descname"><span class="pre">create_or_get_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Union</span><span class="p"><span class="pre">[</span></span><span class="pre">Path</span><span class="p"><span class="pre">,</span></span><span class="w"> </span><span class="pre">str</span><span class="p"><span class="pre">]</span></span></span></em>, <em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">ExportRecord</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.create_or_get_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.create_or_get_file_record" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">create_or_get_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">ExportRecord</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.create_or_get_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.create_or_get_file_record" title="Permalink to this definition">#</a></dt>
<dd><p>create a new record for filename and uuid or return existing record</p>
<p>Returns: an ExportRecord object</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.delete_data_for_filepath">
<span class="sig-name descname"><span class="pre">delete_data_for_filepath</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filepath</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.delete_data_for_filepath"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.delete_data_for_filepath" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">delete_data_for_filepath</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filepath</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.delete_data_for_filepath"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.delete_data_for_filepath" title="Permalink to this definition">#</a></dt>
<dd><p>Delete all exportdb data for given filepath</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.delete_data_for_uuid">
<span class="sig-name descname"><span class="pre">delete_data_for_uuid</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/export_db.html#ExportDB.delete_data_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.delete_data_for_uuid" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">delete_data_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.delete_data_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.delete_data_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>Delete all exportdb data for given UUID</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.ExportDB.export_dir">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">export_dir</span></span><a class="headerlink" href="#osxphotos.ExportDB.export_dir" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">export_dir</span></span><em class="property"><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="pre">str</span></em><a class="headerlink" href="#osxphotos.ExportDB.export_dir" title="Permalink to this definition">#</a></dt>
<dd><p>returns path to export directory</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_export_results">
<span class="sig-name descname"><span class="pre">get_export_results</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</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">0</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_export_results"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_export_results" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">get_export_results</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</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">0</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="s"><span class="pre">'osxphotos.photoexporter.ExportResults'</span></span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">None</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_export_results"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_export_results" title="Permalink to this definition">#</a></dt>
<dd><p>Retrieve export results from database</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
@@ -458,21 +468,21 @@ If called in context manager, returns True (execution is delayed until exiting c
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_file_record">
<span class="sig-name descname"><span class="pre">get_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Union</span><span class="p"><span class="pre">[</span></span><span class="pre">Path</span><span class="p"><span class="pre">,</span></span><span class="w"> </span><span class="pre">str</span><span class="p"><span class="pre">]</span></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">ExportRecord</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_file_record" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">get_file_record</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</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="s"><span class="pre">'ExportRecord'</span></span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">None</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_file_record"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_file_record" title="Permalink to this definition">#</a></dt>
<dd><p>get info for filename</p>
<p>Returns: an ExportRecord object or None if filename not found</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_files_for_uuid">
<span class="sig-name descname"><span class="pre">get_files_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_files_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_files_for_uuid" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">get_files_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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><span class="pre">str</span><span class="p"><span class="pre">]</span></span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_files_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_files_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>query database for UUID and return list of files associated with UUID or empty list</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_photoinfo_for_uuid">
<span class="sig-name descname"><span class="pre">get_photoinfo_for_uuid</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/export_db.html#ExportDB.get_photoinfo_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_photoinfo_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>returns the photoinfo JSON struct for a UUID</p>
<span class="sig-name descname"><span class="pre">get_photoinfo_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">str</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">None</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_photoinfo_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_photoinfo_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>returns the photoinfo JSON string for a UUID or None if not found</p>
</dd></dl>
<dl class="py method">
@@ -483,7 +493,7 @@ If called in context manager, returns True (execution is delayed until exiting c
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_target_for_file">
<span class="sig-name descname"><span class="pre">get_target_for_file</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</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">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Union</span><span class="p"><span class="pre">[</span></span><span class="pre">str</span><span class="p"><span class="pre">,</span></span><span class="w"> </span><span class="pre">Path</span><span class="p"><span class="pre">]</span></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">Optional</span><span class="p"><span class="pre">[</span></span><span class="pre">str</span><span class="p"><span class="pre">]</span></span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_target_for_file"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_target_for_file" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">get_target_for_file</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</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">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">pathlib.Path</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">str</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">str</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">None</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_target_for_file"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_target_for_file" title="Permalink to this definition">#</a></dt>
<dd><dl class="simple">
<dt>query database for file matching file name and return the matching filename if there is one;</dt><dd><p>otherwise return None; looks for file.ext, file (1).ext, file (2).ext and so on to find the
actual target name that was used to export filename</p>
@@ -494,33 +504,33 @@ actual target name that was used to export filename</p>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.get_uuid_for_file">
<span class="sig-name descname"><span class="pre">get_uuid_for_file</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_uuid_for_file"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_uuid_for_file" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">get_uuid_for_file</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">filename</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</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">str</span><span class="w"> </span><span class="p"><span class="pre">|</span></span><span class="w"> </span><span class="pre">None</span></span></span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.get_uuid_for_file"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.get_uuid_for_file" title="Permalink to this definition">#</a></dt>
<dd><p>query database for filename and return UUID
returns None if filename not found in database</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.ExportDB.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.ExportDB.path" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">path</span></span><em class="property"><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="pre">str</span></em><a class="headerlink" href="#osxphotos.ExportDB.path" title="Permalink to this definition">#</a></dt>
<dd><p>returns path to export database</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.set_config">
<span class="sig-name descname"><span class="pre">set_config</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">config_data</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_config"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_config" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">set_config</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">config_data</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_config"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_config" title="Permalink to this definition">#</a></dt>
<dd><p>set config in the database</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.set_export_results">
<span class="sig-name descname"><span class="pre">set_export_results</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">results</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_export_results"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_export_results" title="Permalink to this definition">#</a></dt>
<span class="sig-name descname"><span class="pre">set_export_results</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">results</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><a class="reference internal" href="#osxphotos.ExportResults" title="osxphotos.photoexporter.ExportResults"><span class="pre">ExportResults</span></a></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_export_results"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_export_results" title="Permalink to this definition">#</a></dt>
<dd><p>Store export results in database; data is pickled and gzipped for storage</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ExportDB.set_photoinfo_for_uuid">
<span class="sig-name descname"><span class="pre">set_photoinfo_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">info</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_photoinfo_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_photoinfo_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>sets the photoinfo JSON struct for a UUID</p>
<span class="sig-name descname"><span class="pre">set_photoinfo_for_uuid</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">uuid</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">info</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/export_db.html#ExportDB.set_photoinfo_for_uuid"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ExportDB.set_photoinfo_for_uuid" title="Permalink to this definition">#</a></dt>
<dd><p>sets the photoinfo JSON string for a UUID</p>
</dd></dl>
</dd></dl>
@@ -1184,6 +1194,12 @@ including folders, albums, etc</p>
<dd><p>return list of albums (as AlbumInfo objects) contained in the folder</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.FolderInfo.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/albuminfo.html#FolderInfo.asdict"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.FolderInfo.asdict" title="Permalink to this definition">#</a></dt>
<dd><p>Return folder info as a dict</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.FolderInfo.parent">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">parent</span></span><a class="headerlink" href="#osxphotos.FolderInfo.parent" title="Permalink to this definition">#</a></dt>
@@ -1212,14 +1228,26 @@ including folders, albums, etc</p>
<dl class="py class">
<dt class="sig sig-object py" id="osxphotos.ImportInfo">
<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">ImportInfo</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><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/albuminfo.html#ImportInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ImportInfo" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">ImportInfo</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">db</span></span></em>, <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/albuminfo.html#ImportInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ImportInfo" title="Permalink to this definition">#</a></dt>
<dd><p>Information about import sessions</p>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.ImportInfo.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/albuminfo.html#ImportInfo.asdict"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ImportInfo.asdict" title="Permalink to this definition">#</a></dt>
<dd><p>Return import info as a dict</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.ImportInfo.photos">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">photos</span></span><a class="headerlink" href="#osxphotos.ImportInfo.photos" title="Permalink to this definition">#</a></dt>
<dd><p>return list of photos contained in import session</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.ImportInfo.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.ImportInfo.title" title="Permalink to this definition">#</a></dt>
<dd><p>return title / name of import session</p>
</dd></dl>
</dd></dl>
<dl class="py class">
@@ -1518,8 +1546,8 @@ self is not included in the returned list</p>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.cloud_metadata">
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">cloud_metadata</span></span><em class="property"><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="pre">Dict</span></em><a class="headerlink" href="#osxphotos.PhotoInfo.cloud_metadata" title="Permalink to this definition">#</a></dt>
<dd><p>Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict</p>
<em class="property"><span class="pre">property</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">cloud_metadata</span></span><em class="property"><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="pre">dict</span><span class="p"><span class="pre">[</span></span><span class="pre">Any</span><span class="p"><span class="pre">,</span></span><span class="w"> </span><span class="pre">Any</span><span class="p"><span class="pre">]</span></span></em><a class="headerlink" href="#osxphotos.PhotoInfo.cloud_metadata" title="Permalink to this definition">#</a></dt>
<dd><p>Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict; Photos 5+ only</p>
</dd></dl>
<dl class="py property">
@@ -2545,7 +2573,7 @@ Returns photos regardless of intrash state.</p>
<dl class="py class">
<dt class="sig sig-object py" id="osxphotos.ProjectInfo">
<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">ProjectInfo</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><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/albuminfo.html#ProjectInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ProjectInfo" title="Permalink to this definition">#</a></dt>
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">osxphotos.</span></span><span class="sig-name descname"><span class="pre">ProjectInfo</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">db</span></span></em>, <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/albuminfo.html#ProjectInfo"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.ProjectInfo" title="Permalink to this definition">#</a></dt>
<dd><p>ProjectInfo with info about projects
Projects are cards, calendars, slideshows, etc.</p>
</dd></dl>
@@ -3663,6 +3691,7 @@ Projects are cards, calendars, slideshows, etc.</p>
<ul>
<li><a class="reference internal" href="#">OSXPhotos Python Reference</a><ul>
<li><a class="reference internal" href="#osxphotos.AlbumInfo"><code class="docutils literal notranslate"><span class="pre">AlbumInfo</span></code></a><ul>
<li><a class="reference internal" href="#osxphotos.AlbumInfo.asdict"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.asdict()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.AlbumInfo.folder_list"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_list</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.AlbumInfo.folder_names"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_names</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.AlbumInfo.parent"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.parent</span></code></a></li>
@@ -3687,6 +3716,7 @@ Projects are cards, calendars, slideshows, etc.</p>
</li>
<li><a class="reference internal" href="#osxphotos.ExportDB"><code class="docutils literal notranslate"><span class="pre">ExportDB</span></code></a><ul>
<li><a class="reference internal" href="#osxphotos.ExportDB.close"><code class="docutils literal notranslate"><span class="pre">ExportDB.close()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ExportDB.connection"><code class="docutils literal notranslate"><span class="pre">ExportDB.connection</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ExportDB.create_file_record"><code class="docutils literal notranslate"><span class="pre">ExportDB.create_file_record()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ExportDB.create_or_get_file_record"><code class="docutils literal notranslate"><span class="pre">ExportDB.create_or_get_file_record()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ExportDB.delete_data_for_filepath"><code class="docutils literal notranslate"><span class="pre">ExportDB.delete_data_for_filepath()</span></code></a></li>
@@ -3776,6 +3806,7 @@ Projects are cards, calendars, slideshows, etc.</p>
</li>
<li><a class="reference internal" href="#osxphotos.FolderInfo"><code class="docutils literal notranslate"><span class="pre">FolderInfo</span></code></a><ul>
<li><a class="reference internal" href="#osxphotos.FolderInfo.album_info"><code class="docutils literal notranslate"><span class="pre">FolderInfo.album_info</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.FolderInfo.asdict"><code class="docutils literal notranslate"><span class="pre">FolderInfo.asdict()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.FolderInfo.parent"><code class="docutils literal notranslate"><span class="pre">FolderInfo.parent</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.FolderInfo.subfolders"><code class="docutils literal notranslate"><span class="pre">FolderInfo.subfolders</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.FolderInfo.title"><code class="docutils literal notranslate"><span class="pre">FolderInfo.title</span></code></a></li>
@@ -3783,7 +3814,9 @@ Projects are cards, calendars, slideshows, etc.</p>
</ul>
</li>
<li><a class="reference internal" href="#osxphotos.ImportInfo"><code class="docutils literal notranslate"><span class="pre">ImportInfo</span></code></a><ul>
<li><a class="reference internal" href="#osxphotos.ImportInfo.asdict"><code class="docutils literal notranslate"><span class="pre">ImportInfo.asdict()</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ImportInfo.photos"><code class="docutils literal notranslate"><span class="pre">ImportInfo.photos</span></code></a></li>
<li><a class="reference internal" href="#osxphotos.ImportInfo.title"><code class="docutils literal notranslate"><span class="pre">ImportInfo.title</span></code></a></li>
</ul>
</li>
<li><a class="reference internal" href="#osxphotos.LikeInfo"><code class="docutils literal notranslate"><span class="pre">LikeInfo</span></code></a></li>

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -361,7 +361,7 @@ Template Substitutions
* - {tab}
- :A tab: '\t'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.58.2'
- The osxphotos version, e.g. '0.59.0'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}

View File

@@ -0,0 +1,52 @@
"""Example for concurrent export of photos using osxphotos.PhotoExporter.export()
Note: concurrent export can only be used on Python 3.11 and later due to the way
python's sqlite3 module is implemented. See https://docs.python.org/3/library/sqlite3.html#sqlite3.threadsafety
for more information.
"""
import concurrent.futures
import os
import time
import click
import osxphotos
from osxphotos.cli import echo, query_command, verbose
@query_command()
@click.option(
"--workers",
metavar="WORKERS",
help="Maximum number of worker threads to use for export. "
"If not specified, it will default to the number of processors on the machine, multiplied by 5.",
type=int,
)
@click.argument(
"export_dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True),
)
def export(workers, export_dir, photos: list[osxphotos.PhotoInfo], **kwargs):
"""Export photos to EXPORT_DIR using concurrent export.
Use --workers to specify the number of worker threads to use.
This simple example exports only the original photo and does not export
edited versions, live photos, etc.
"""
workers = workers or os.cpu_count() * 5
echo(f"Exporting {len(photos)} photos to {export_dir} using {workers} workers")
start_t = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(p.export, export_dir) for p in photos]
exported = []
for future in concurrent.futures.as_completed(futures):
exported.extend(future.result())
end_t = time.perf_counter()
echo(
f"Exported {len(exported)} photos to {export_dir} in {end_t-start_t:.4f} seconds"
)
if __name__ == "__main__":
export()

View File

@@ -2,10 +2,14 @@
from __future__ import annotations
import logging
import os.path
import sqlite3
from datetime import datetime
from enum import Enum
logger: logging.Logger = logging.getLogger("osxphotos")
APP_NAME = "osxphotos"
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
@@ -464,3 +468,12 @@ PROFILE_SORT_KEYS = [
UUID_PATTERN = (
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
)
# Reference: https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.threadsafety
# and https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.connect
# 3: serialized mode; Threads may share the module, connections and cursors
# 3 is the default in the python.org python 3.11 distribution
# earlier versions of python.org python 3.x default to 1 which means threads may not share
# sqlite3 connections and thus PhotoInfo.export() cannot be used in a multithreaded environment
# pass SQLITE_CHECK_SAME_THREAD to sqlite3.connect() to enable multithreaded access on systems that support it
SQLITE_CHECK_SAME_THREAD = not sqlite3.threadsafety == 3
logger.debug(f"{SQLITE_CHECK_SAME_THREAD=}, {sqlite3.threadsafety=}")

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.58.2"
__version__ = "0.59.0"

View File

@@ -18,6 +18,7 @@ from ._constants import (
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_VERSION,
TIME_DELTA,
AlbumSortOrder,
)
@@ -61,7 +62,7 @@ class AlbumInfoBaseClass:
including folders, photos, etc.
"""
def __init__(self, db=None, uuid=None):
def __init__(self, db, uuid):
self._uuid = uuid
self._db = db
self._title = self._db._dbalbum_details[uuid]["title"]
@@ -121,7 +122,8 @@ class AlbumInfoBaseClass:
@property
def end_date(self):
"""For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed)"""
For Import Sessions, return end date of import sessions (when import was completed)
"""
try:
return self._end_date
except AttributeError:
@@ -163,6 +165,17 @@ class AlbumInfoBaseClass:
self._owner = None
return self._owner
def asdict(self):
"""Return album info as a dict"""
return {
"uuid": self.uuid,
"creation_date": self.creation_date,
"start_date": self.start_date,
"end_date": self.end_date,
"owner": self.owner,
"photos": [p.uuid for p in self.photos],
}
def __len__(self):
"""return number of photos contained in album"""
return len(self.photos)
@@ -174,6 +187,10 @@ class AlbumInfo(AlbumInfoBaseClass):
including folders, photos, etc.
"""
def __init__(self, db, uuid):
super().__init__(db=db, uuid=uuid)
self._title = self._db._dbalbum_details[uuid]["title"]
@property
def title(self):
"""return title / name of album"""
@@ -205,10 +222,11 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def folder_names(self):
"""return hierarchical list of folders the album is contained in
"""Return hierarchical list of folders the album is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
or empty list if album is not in any folders
"""
try:
return self._folder_names
@@ -218,10 +236,9 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def folder_list(self):
"""return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
"""Returns list of FolderInfo objects for each folder the album is contained in
or empty list if album is not in any folders
"""
try:
return self._folders
@@ -246,7 +263,7 @@ class AlbumInfo(AlbumInfoBaseClass):
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"]
self._parent = (
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk])
if parent_pk != self._db._folder_root_pk
if parent_pk is not None and parent_pk != self._db._folder_root_pk
else None
)
return self._parent
@@ -281,27 +298,80 @@ class AlbumInfo(AlbumInfoBaseClass):
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
def asdict(self):
"""Return album info as a dict"""
dict_data = super().asdict()
dict_data["title"] = self.title
dict_data["folder_names"] = self.folder_names
dict_data["folder_list"] = [f.uuid for f in self.folder_list]
dict_data["sort_order"] = self.sort_order
dict_data["parent"] = self.parent.uuid if self.parent else None
return dict_data
class ImportInfo(AlbumInfoBaseClass):
"""Information about import sessions"""
def __init__(self, db, uuid):
self._uuid = uuid
self._db = db
if self._db._db_version >= _PHOTOS_5_VERSION:
return super().__init__(db=db, uuid=uuid)
import_session = self._db._db_import_group[self._uuid]
try:
self._creation_date_timestamp = import_session[3]
except (ValueError, TypeError, KeyError):
self._creation_date_timestamp = datetime(1970, 1, 1)
self._start_date_timestamp = self._creation_date_timestamp
self._end_date_timestamp = self._creation_date_timestamp
self._title = import_session[2]
self._local_tz = get_local_tz(
datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
)
@property
def title(self):
"""return title / name of import session"""
return self._title
@property
def photos(self):
"""return list of photos contained in import session"""
try:
return self._photos
except AttributeError:
uuid_list, sort_order = zip(
*[
(uuid, self._db._dbphotos[uuid]["fok_import_session"])
for uuid in self._db._dbphotos
if self._db._dbphotos[uuid]["import_uuid"] == self.uuid
if self._db._db_version >= _PHOTOS_5_VERSION:
uuid_list, sort_order = zip(
*[
(uuid, self._db._dbphotos[uuid]["fok_import_session"])
for uuid in self._db._dbphotos
if self._db._dbphotos[uuid]["import_uuid"] == self.uuid
]
)
sorted_uuid = sort_list_by_keys(uuid_list, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
else:
import_photo_uuids = [
u
for u in self._db._dbphotos
if self._db._dbphotos[u]["import_uuid"] == self.uuid
]
)
sorted_uuid = sort_list_by_keys(uuid_list, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
self._photos = self._db.photos_by_uuid(import_photo_uuids)
return self._photos
def asdict(self):
"""Return import info as a dict"""
return {
"uuid": self.uuid,
"creation_date": self.creation_date,
"start_date": self.start_date,
"end_date": self.end_date,
"title": self.title,
"photos": [p.uuid for p in self.photos],
}
def __bool__(self):
"""Always returns True
A photo without an import session will return None for import_info,
@@ -309,6 +379,7 @@ class ImportInfo(AlbumInfoBaseClass):
"""
return True
class ProjectInfo(AlbumInfo):
"""
ProjectInfo with info about projects
@@ -386,7 +457,7 @@ class FolderInfo:
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"]
self._parent = (
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk])
if parent_pk != self._db._folder_root_pk
if parent_pk is not None and parent_pk != self._db._folder_root_pk
else None
)
return self._parent
@@ -416,6 +487,16 @@ class FolderInfo:
self._folders = folders
return self._folders
def asdict(self):
"""Return folder info as a dict"""
return {
"title": self.title,
"uuid": self.uuid,
"parent": self.parent.uuid if self.parent is not None else None,
"subfolders": [f.uuid for f in self.subfolders],
"albums": [a.uuid for a in self.album_info],
}
def __len__(self):
"""returns count of folders + albums contained in the folder"""
return len(self.subfolders) + len(self.album_info)

View File

@@ -88,7 +88,7 @@ def batch_edit(
This will set the title to "California vacation 2023 2023-02-20 001", and so on,
the description to the reverse geolocation place name,
and add the keywords to "Family", "Travel".
and add the keywords "Family" and "Travel".
--title, --description, and --keyword may be any valid template string.
See https://rhettbull.github.io/osxphotos/template_help.html

View File

@@ -1,5 +1,7 @@
"""export command for osxphotos CLI"""
from __future__ import annotations
import atexit
import inspect
import os
@@ -9,7 +11,8 @@ import shlex
import subprocess
import sys
import time
from typing import Iterable, List, Optional, Tuple
from typing import Iterable, List, Optional, Tuple, Any, Callable
import concurrent.futures
import click
from osxmetadata import (
@@ -1426,173 +1429,156 @@ def export(
photo_num = 0
num_exported = 0
# hack to avoid passing all the options to export_photo
kwargs = locals().copy()
kwargs["export_dir"] = dest
kwargs["export_preview"] = preview
limit_str = f" (limit = [num]{limit}[/num])" if limit else ""
with rich_progress(console=get_verbose_console(), mock=no_progress) as progress:
task = progress.add_task(
f"Exporting [num]{num_photos}[/] photos{limit_str}", total=num_photos
)
for p in photos:
photo_num += 1
# hack to avoid passing all the options to export_photo
kwargs = {
k: v
for k, v in locals().items()
if k in inspect.getfullargspec(export_photo).args
}
kwargs["photo"] = p
kwargs["export_dir"] = dest
kwargs["export_preview"] = preview
export_results = export_photo(**kwargs)
if post_function:
for function in post_function:
# post function is tuple of (function, filename.py::function_name)
verbose(f"Calling post-function [bold]{function[1]}")
if not dry_run:
try:
function[0](p, export_results, verbose)
except Exception as e:
rich_echo_error(
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
futures = []
with concurrent.futures.ThreadPoolExecutor(
# max_workers=os.cpu_count()
max_workers=1,
) as executor:
for p in photos:
photo_num += 1
kwargs["photo_num"] = photo_num
futures.append(executor.submit(export_worker, p, **kwargs))
for future in concurrent.futures.as_completed(futures):
p, export_results = future.result()
if album_export and export_results.exported:
try:
album_export.add(p)
export_results.exported_album = [
(filename, album_export.name)
for filename in export_results.exported
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_export.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
if album_skipped and export_results.skipped:
try:
album_skipped.add(p)
export_results.skipped_album = [
(filename, album_skipped.name)
for filename in export_results.skipped
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_skipped.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
if album_missing and export_results.missing:
try:
album_missing.add(p)
export_results.missing_album = [
(filename, album_missing.name)
for filename in export_results.missing
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_missing.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
results += export_results
# all photo files (not including sidecars) that are part of this export set
# used below for applying Finder tags, etc.
photo_files = set(
export_results.exported
+ export_results.new
+ export_results.updated
+ export_results.exif_updated
+ export_results.converted_to_jpeg
+ export_results.skipped
)
if finder_tag_keywords or finder_tag_template:
if dry_run:
for filepath in photo_files:
verbose(
f"Writing Finder tags to [filepath]{filepath}[/]"
)
run_post_command(
photo=p,
post_command=post_command,
export_results=export_results,
export_dir=dest,
dry_run=dry_run,
exiftool_path=exiftool_path,
export_db=export_db,
verbose=verbose,
)
if album_export and export_results.exported:
try:
album_export.add(p)
export_results.exported_album = [
(filename, album_export.name)
for filename in export_results.exported
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_export.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
if album_skipped and export_results.skipped:
try:
album_skipped.add(p)
export_results.skipped_album = [
(filename, album_skipped.name)
for filename in export_results.skipped
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_skipped.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
if album_missing and export_results.missing:
try:
album_missing.add(p)
export_results.missing_album = [
(filename, album_missing.name)
for filename in export_results.missing
]
except Exception as e:
click.secho(
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_missing.name}: {e}",
fg=CLI_COLOR_ERROR,
err=True,
)
results += export_results
# all photo files (not including sidecars) that are part of this export set
# used below for applying Finder tags, etc.
photo_files = set(
export_results.exported
+ export_results.new
+ export_results.updated
+ export_results.exif_updated
+ export_results.converted_to_jpeg
+ export_results.skipped
)
if finder_tag_keywords or finder_tag_template:
if dry_run:
for filepath in photo_files:
verbose(f"Writing Finder tags to [filepath]{filepath}[/]")
else:
tags_written, tags_skipped = write_finder_tags(
p,
photo_files,
keywords=finder_tag_keywords,
keyword_template=keyword_template,
album_keyword=album_keyword,
person_keyword=person_keyword,
exiftool_merge_keywords=exiftool_merge_keywords,
finder_tag_template=finder_tag_template,
strip=strip,
export_dir=dest,
verbose=verbose,
)
export_results.xattr_written.extend(tags_written)
export_results.xattr_skipped.extend(tags_skipped)
results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped)
if xattr_template:
if dry_run:
for filepath in photo_files:
verbose(
f"Writing extended attributes to [filepath]{filepath}[/]"
else:
tags_written, tags_skipped = write_finder_tags(
p,
photo_files,
keywords=finder_tag_keywords,
keyword_template=keyword_template,
album_keyword=album_keyword,
person_keyword=person_keyword,
exiftool_merge_keywords=exiftool_merge_keywords,
finder_tag_template=finder_tag_template,
strip=strip,
export_dir=dest,
verbose=verbose,
)
else:
xattr_written, xattr_skipped = write_extended_attributes(
p,
photo_files,
xattr_template,
strip=strip,
export_dir=dest,
verbose=verbose,
)
export_results.xattr_written.extend(xattr_written)
export_results.xattr_skipped.extend(xattr_skipped)
results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped)
export_results.xattr_written.extend(tags_written)
export_results.xattr_skipped.extend(tags_skipped)
results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped)
report_writer.write(export_results)
if print_template:
options = RenderOptions(export_dir=dest)
for template in print_template:
rendered_templates, unmatched = p.render_template(
template,
options,
)
if unmatched:
rich_click_echo(
f"[warning]Unmatched template field: {unmatched}[/]"
if xattr_template:
if dry_run:
for filepath in photo_files:
verbose(
f"Writing extended attributes to [filepath]{filepath}[/]"
)
else:
xattr_written, xattr_skipped = write_extended_attributes(
p,
photo_files,
xattr_template,
strip=strip,
export_dir=dest,
verbose=verbose,
)
for rendered_template in rendered_templates:
if not rendered_template:
continue
rich_click_echo(rendered_template)
export_results.xattr_written.extend(xattr_written)
export_results.xattr_skipped.extend(xattr_skipped)
results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped)
progress.advance(task)
report_writer.write(export_results)
# handle limit
if export_results.exported:
# if any photos were exported, increment num_exported used by limit
# limit considers each PhotoInfo object as a single photo even if multiple files are exported
num_exported += 1
if limit and num_exported >= limit:
# advance progress to end
progress.advance(task, num_photos - photo_num)
break
if print_template:
options = RenderOptions(export_dir=dest)
for template in print_template:
rendered_templates, unmatched = p.render_template(
template,
options,
)
if unmatched:
rich_click_echo(
f"[warning]Unmatched template field: {unmatched}[/]"
)
for rendered_template in rendered_templates:
if not rendered_template:
continue
rich_click_echo(rendered_template)
progress.advance(task)
# handle limit
if export_results.exported:
# if any photos were exported, increment num_exported used by limit
# limit considers each PhotoInfo object as a single photo even if multiple files are exported
num_exported += 1
if limit and num_exported >= limit:
# advance progress to end
progress.advance(task, num_photos - photo_num)
break
photo_str_total = pluralize(len(photos), "photo", "photos")
if update or force_update:
@@ -1682,6 +1668,45 @@ def export(
export_db.close()
def export_worker(
photo: osxphotos.PhotoInfo, **kwargs
) -> tuple[osxphotos.PhotoInfo, ExportResults]:
"""Export worker function for multi-threaded export of photos"""
dry_run = kwargs["dry_run"]
verbose: Callable[[str], Any] = kwargs["verbose"]
export_args = {
k: v
for k, v in kwargs.items()
if k in inspect.getfullargspec(export_photo).args
}
export_args["photo"] = photo
export_results = export_photo(**export_args)
if post_function := kwargs["post_function"]:
for function in post_function:
# post function is tuple of (function, filename.py::function_name)
verbose(f"Calling post-function [bold]{function[1]}")
if not dry_run:
try:
function[0](photo, export_results, verbose)
except Exception as e:
rich_echo_error(
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
)
run_post_command(
photo=photo,
post_command=kwargs["post_command"],
export_results=export_results,
export_dir=kwargs["dest"],
dry_run=dry_run,
exiftool_path=kwargs["exiftool_path"],
export_db=kwargs["export_db"],
verbose=verbose,
)
return photo, export_results
def export_photo(
photo=None,
dest=None,

View File

@@ -25,7 +25,7 @@ from rich.console import Console
from rich.markdown import Markdown
from strpdatetime import strpdatetime
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL, SQLITE_CHECK_SAME_THREAD
from osxphotos._version import __version__
from osxphotos.cli.cli_params import TIMESTAMP_OPTION, VERBOSE_OPTION
from osxphotos.cli.common import get_data_dir
@@ -77,7 +77,8 @@ def echo(message, emoji=True, **kwargs):
class PhotoInfoFromFile:
"""Mock PhotoInfo class for a file to be imported
Returns None for most attributes but allows some templates like exiftool and created to work correctly"""
Returns None for most attributes but allows some templates like exiftool and created to work correctly
"""
def __init__(self, filepath: Union[str, Path], exiftool: Optional[str] = None):
self._path = str(filepath)
@@ -745,7 +746,7 @@ def write_sqlite_report(
file_exists = os.path.isfile(report_file)
conn = sqlite3.connect(report_file)
conn = sqlite3.connect(report_file, check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
if not append or not file_exists:

View File

@@ -12,6 +12,7 @@ from abc import ABC, abstractmethod
from contextlib import suppress
from typing import Dict, Union
from osxphotos._constants import SQLITE_CHECK_SAME_THREAD
from osxphotos.export_db import OSXPHOTOS_ABOUT_STRING
from osxphotos.photoexporter import ExportResults
from osxphotos.sqlite_utils import sqlite_columns
@@ -181,7 +182,7 @@ class ExportReportWriterSQLite(ReportWriterABC):
with suppress(FileNotFoundError):
os.unlink(self.output_file)
self._conn = sqlite3.connect(self.output_file)
self._conn = sqlite3.connect(self.output_file, check_same_thread=SQLITE_CHECK_SAME_THREAD)
self._create_tables()
self.report_id = self._generate_report_id()
@@ -533,7 +534,7 @@ class SyncReportWriterSQLite(ReportWriterABC):
with suppress(FileNotFoundError):
os.unlink(self.output_file)
self._conn = sqlite3.connect(self.output_file)
self._conn = sqlite3.connect(self.output_file, check_same_thread=SQLITE_CHECK_SAME_THREAD)
self._create_tables()
self.report_id = self._generate_report_id()

Binary file not shown.

View File

@@ -1,11 +1,11 @@
""" Yet another simple exiftool wrapper
I rolled my own for following reasons:
1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)
2. I wanted singleton behavior so only a single exiftool process was ever running
2. I wanted exiftool processes to stay resident between calls (improved performance)
3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance)
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
"""
from __future__ import annotations
import atexit
import contextlib
@@ -17,6 +17,7 @@ import pathlib
import re
import shutil
import subprocess
import threading
from abc import ABC, abstractmethod
from functools import lru_cache # pylint: disable=syntax-error
@@ -30,6 +31,8 @@ __all__ = [
"unescape_str",
]
logger = logging.getLogger("osxphotos")
# exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}"
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
@@ -42,6 +45,8 @@ EXIFTOOL_FILETYPES_JSON = "exiftool_filetypes.json"
with (pathlib.Path(__file__).parent / EXIFTOOL_FILETYPES_JSON).open("r") as f:
EXIFTOOL_SUPPORTED_FILETYPES = json.load(f)
NUM_PROCESSES = os.cpu_count() or 1
def exiftool_can_write(suffix: str) -> bool:
"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""
@@ -96,8 +101,11 @@ def get_exiftool_path():
class _ExifToolProc:
"""Runs exiftool in a subprocess via Popen
Creates a singleton object"""
"""
Runs exiftool in a subprocess via Popen
Creates a singleton object that dispatches commands to one or
more exiftool subprocesses.
"""
def __new__(cls, *args, **kwargs):
"""create new object or return instance of already created singleton"""
@@ -106,7 +114,11 @@ class _ExifToolProc:
return cls.instance
def __init__(self, exiftool=None, large_file_support=True):
def __init__(
self,
exiftool: str | None = None,
large_file_support: bool = True,
):
"""construct _ExifToolProc singleton object or return instance of already created object
Args:
@@ -117,7 +129,7 @@ class _ExifToolProc:
if hasattr(self, "_process_running") and self._process_running:
# already running
if exiftool is not None and exiftool != self._exiftool:
logging.warning(
logger.warning(
f"exiftool subprocess already running, "
f"ignoring exiftool={exiftool}"
)
@@ -125,6 +137,9 @@ class _ExifToolProc:
self._process_running = False
self._large_file_support = large_file_support
self._exiftool = exiftool or get_exiftool_path()
self._num_processes = NUM_PROCESSES
self._process = []
self._process_counter = 0
self._start_proc(large_file_support=large_file_support)
@property
@@ -132,23 +147,20 @@ class _ExifToolProc:
"""return the exiftool subprocess"""
if not self._process_running:
self._start_proc(large_file_support=self._large_file_support)
return self._process
@property
def pid(self):
"""return process id (PID) of the exiftool process"""
return self._process.pid
process_idx = self._process_counter % self._num_processes
self._process_counter += 1
return self._process[process_idx]
@property
def exiftool(self):
"""return path to exiftool process"""
return self._exiftool
def _start_proc(self, large_file_support):
def _start_proc(self, large_file_support: bool):
"""start exiftool in batch mode"""
if self._process_running:
logging.warning("exiftool already running: {self._process}")
logger.debug(f"exiftool already running: {self._process}")
return
# open exiftool process
@@ -156,25 +168,28 @@ class _ExifToolProc:
env = os.environ.copy()
env["PATH"] = f'/usr/bin/:{env["PATH"]}'
large_file_args = ["-api", "largefilesupport=1"] if large_file_support else []
self._process = subprocess.Popen(
[
self._exiftool,
"-stay_open", # keep process open in batch mode
"True", # -stay_open=True, keep process open in batch mode
*large_file_args,
"-@", # read command-line arguments from file
"-", # read from stdin
"-common_args", # specifies args common to all commands subsequently run
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-P", # Preserve file modification date/time
"-G", # print group name for each tag
"-E", # escape tag values for HTML (allows use of HTML &#xa; for newlines)
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
for _ in range(self._num_processes):
self._process.append(
subprocess.Popen(
[
self._exiftool,
"-stay_open", # keep process open in batch mode
"True", # -stay_open=True, keep process open in batch mode
*large_file_args,
"-@", # read command-line arguments from file
"-", # read from stdin
"-common_args", # specifies args common to all commands subsequently run
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-P", # Preserve file modification date/time
"-G", # print group name for each tag
"-E", # escape tag values for HTML (allows use of HTML &#xa; for newlines)
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
)
self._process_running = True
EXIFTOOL_PROCESSES.append(self)
@@ -185,17 +200,19 @@ class _ExifToolProc:
if not self._process_running:
return
with contextlib.suppress(Exception):
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
try:
self._process.communicate(timeout=5)
except subprocess.TimeoutExpired:
self._process.kill()
self._process.communicate()
for i in range(self._num_processes):
process = self._process[i]
with contextlib.suppress(Exception):
process.stdin.write(b"-stay_open\n")
process.stdin.write(b"False\n")
process.stdin.flush()
try:
process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.communicate()
del self._process
self._process = []
self._process_running = False
@@ -233,6 +250,7 @@ class ExifTool:
self._exiftoolproc = _ExifToolProc(
exiftool=exiftool, large_file_support=large_file_support
)
self._lock = threading.Lock()
self._read_exif()
@property
@@ -336,57 +354,54 @@ class ExifTool:
if not commands:
raise TypeError("must provide one or more command to run")
if self._context_mgr and self.overwrite:
commands = list(commands)
commands.append("-overwrite_original")
with self._lock:
if self._context_mgr and self.overwrite:
commands = list(commands)
commands.append("-overwrite_original")
filename = b"" if no_file else os.fsencode(self.file)
filename = b"" if no_file else os.fsencode(self.file)
if self.flags:
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
flags = []
for f in self.flags:
flags.extend(f.split())
command_str = b"\n".join([f.encode("utf-8") for f in flags])
command_str += b"\n"
else:
command_str = b""
command_str += (
b"\n".join([c.encode("utf-8") for c in commands])
+ b"\n"
+ filename
+ b"\n"
+ b"-execute\n"
)
# send the command
self._process.stdin.write(command_str)
self._process.stdin.flush()
# read the output
output = b""
warning = b""
error = b""
while EXIFTOOL_STAYOPEN_EOF not in str(output):
line = self._process.stdout.readline()
if line.startswith(b"Warning"):
warning += line.strip()
elif line.startswith(b"Error"):
error += line.strip()
if self.flags:
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
flags = []
for f in self.flags:
flags.extend(f.split())
command_str = b"\n".join([f.encode("utf-8") for f in flags])
command_str += b"\n"
else:
output += line.strip()
warning = "" if warning == b"" else warning.decode("utf-8")
error = "" if error == b"" else error.decode("utf-8")
self.warning = warning
self.error = error
command_str = b""
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN], warning, error
command_str += (
b"\n".join([c.encode("utf-8") for c in commands])
+ b"\n"
+ filename
+ b"\n"
+ b"-execute\n"
)
@property
def pid(self):
"""return process id (PID) of the exiftool process"""
return self._process.pid
# send the command
process = self._process
process.stdin.write(command_str)
process.stdin.flush()
# read the output
output = b""
warning = b""
error = b""
while EXIFTOOL_STAYOPEN_EOF not in str(output):
line = process.stdout.readline()
if line.startswith(b"Warning"):
warning += line.strip()
elif line.startswith(b"Error"):
error += line.strip()
else:
output += line.strip()
warning = "" if warning == b"" else warning.decode("utf-8")
error = "" if error == b"" else error.decode("utf-8")
self.warning = warning
self.error = error
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN], warning, error
@property
def version(self):
@@ -404,7 +419,7 @@ class ExifTool:
"""
json_str, _, _ = self.run_commands("-json")
if not json_str:
return dict()
return {}
json_str = unescape_str(json_str.decode("utf-8"))
try:
@@ -412,8 +427,8 @@ 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()
logger.warning(f"error loading json returned by exiftool: {e} {json_str}")
return {}
exifdict = exifdict[0]
if not tag_groups:
# strip tag groups
@@ -482,7 +497,12 @@ class _ExifToolCaching(ExifTool):
"""
self._json_cache = None
self._asdict_cache = {}
super().__init__(filepath, exiftool=exiftool, overwrite=False, flags=None)
super().__init__(
filepath,
exiftool=exiftool,
overwrite=False,
flags=None,
)
def run_commands(self, *commands, no_file=False):
if commands[0] not in ["-json", "-ver"]:

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ from rich import print
from osxphotos.photoinfo import PhotoInfo
from ._constants import OSXPHOTOS_EXPORT_DB
from ._constants import OSXPHOTOS_EXPORT_DB, SQLITE_CHECK_SAME_THREAD
from ._version import __version__
from .configoptions import ConfigOptions
from .export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
@@ -50,7 +50,7 @@ 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))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
if row := c.execute(
"SELECT osxphotos, exportdb FROM version ORDER BY id DESC LIMIT 1;"
@@ -61,7 +61,7 @@ def export_db_get_version(
def export_db_vacuum(dbfile: Union[str, pathlib.Path]) -> None:
"""Vacuum export database"""
conn = sqlite3.connect(str(dbfile))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
c.execute("VACUUM;")
conn.commit()
@@ -79,7 +79,7 @@ def export_db_update_signatures(
"""
export_dir = pathlib.Path(export_dir)
fileutil = FileUtil
conn = sqlite3.connect(str(dbfile))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
c.execute("SELECT filepath_normalized, filepath FROM export_data;")
rows = c.fetchall()
@@ -114,7 +114,7 @@ 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))
conn = sqlite3.connect(str(export_db), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
if row := c.execute(
"SELECT datetime, args FROM runs ORDER BY id DESC LIMIT 1;"
@@ -127,7 +127,7 @@ def export_db_get_errors(
export_db: Union[str, pathlib.Path]
) -> Tuple[Optional[str], Optional[str]]:
"""Get errors from export database"""
conn = sqlite3.connect(str(export_db))
conn = sqlite3.connect(str(export_db), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
results = c.execute(
"SELECT filepath, uuid, timestamp, error FROM export_data WHERE error is not null ORDER BY timestamp DESC;"
@@ -145,7 +145,7 @@ def export_db_save_config_to_file(
"""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))
conn = sqlite3.connect(str(export_db), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
if not row:
@@ -163,7 +163,7 @@ def export_db_get_config(
export_db: path to export database
override: if True, any loaded config values will overwrite existing values in config
"""
conn = sqlite3.connect(str(export_db))
conn = sqlite3.connect(str(export_db), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
return (
@@ -184,7 +184,7 @@ def export_db_check_signatures(
"""
export_dir = pathlib.Path(export_dir)
fileutil = FileUtil
conn = sqlite3.connect(str(dbfile))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
c.execute("SELECT filepath_normalized, filepath FROM export_data;")
rows = c.fetchall()
@@ -236,7 +236,7 @@ def export_db_touch_files(
)
exportdb.close()
conn = sqlite3.connect(str(dbfile))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
if row := c.execute(
"SELECT config FROM config ORDER BY id DESC LIMIT 1;"
@@ -318,7 +318,7 @@ def export_db_migrate_photos_library(
and update the UUIDs in the export database
"""
verbose(f"Loading data from export database {dbfile}")
conn = sqlite3.connect(str(dbfile))
conn = sqlite3.connect(str(dbfile), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
results = c.execute("SELECT uuid, photoinfo FROM photoinfo;").fetchall()
exportdb_uuids = {}
@@ -495,7 +495,7 @@ def export_db_get_last_library(dbpath: Union[str, pathlib.Path]) -> str:
str: name of library used to export from or "" if not found
"""
dbpath = pathlib.Path(dbpath)
conn = sqlite3.connect(str(dbpath))
conn = sqlite3.connect(str(dbpath), check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
if results := c.execute(
"""

View File

@@ -0,0 +1,169 @@
"""Freeze a PhotoInfo object to allow it to be used in concurrent.futures."""
from __future__ import annotations
import datetime
import json
import logging
import os
import re
from types import SimpleNamespace
from typing import Any
from osxmetadata import OSXMetaData
import osxphotos
from ._constants import TEXT_DETECTION_CONFIDENCE_THRESHOLD
from .exiftool import ExifToolCaching, get_exiftool_path
from .phototemplate import PhotoTemplate, RenderOptions
from .text_detection import detect_text
def frozen_photoinfo_factory(photo: "osxphotos.photoinfo.PhotoInfo") -> SimpleNamespace:
"""Return a frozen SimpleNamespace object for a PhotoInfo object"""
photo_json = photo.json()
def _object_hook(d: dict[Any, Any]):
if not d:
return d
# if d key matches a ISO 8601 datetime ('2023-03-24T06:46:57.690786', '2019-07-04T16:24:01-07:00', '2019-07-04T16:24:01+07:00'), convert to datetime
# fromisoformat will also handle dates with timezone offset in form +0700, etc.
for k, v in d.items():
if isinstance(v, str) and re.match(
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]?\d*[+-]?\d{2}[:]?\d{2}?", v
):
d[k] = datetime.datetime.fromisoformat(v)
return SimpleNamespace(**d)
frozen = json.loads(photo_json, object_hook=lambda d: _object_hook(d))
# add on json() method to frozen object
def _json(*args):
return photo_json
frozen.json = _json
# add hexdigest property to frozen object
frozen.hexdigest = photo.hexdigest
# add on detected_text method to frozen object
frozen = _add_detected_text(frozen)
# add on exiftool property to frozen object
frozen = _add_exiftool(frozen, photo)
# add on render_template method to frozen object
frozen = _add_render_template(frozen)
# add on the _db property to frozen object
# frozen objects don't really have a _db class but some things expect it (e.g. _db._beta)
frozen._db = SimpleNamespace(_beta=photo._db._beta)
return frozen
def _add_detected_text(frozen: SimpleNamespace) -> SimpleNamespace:
"""Add detected_text method to frozen PhotoInfo object"""
def detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)
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
If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available
Returns: list of (detected text, confidence) tuples
"""
try:
return frozen._detected_text_cache[confidence_threshold]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
frozen._detected_text_cache = {}
try:
detected_text = frozen._detected_text()
except Exception as e:
logging.warning(f"Error detecting text in photo {frozen.uuid}: {e}")
detected_text = []
frozen._detected_text_cache[confidence_threshold] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return frozen._detected_text_cache[confidence_threshold]
def _detected_text():
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
path = (
frozen.path_edited
if frozen.hasadjustments and frozen.path_edited
else frozen.path
)
path = path or frozen.path_derivatives[0] if frozen.path_derivatives else None
if not path:
return []
md = OSXMetaData(path)
try:
def decoder(val):
"""Decode value from JSON"""
return json.loads(val.decode("utf-8"))
detected_text = md.get_xattr(
"osxphotos.metadata:detected_text", decode=decoder
)
except KeyError:
detected_text = None
if detected_text is None:
orientation = frozen.orientation or None
detected_text = detect_text(path, orientation)
def encoder(obj):
"""Encode value as JSON"""
val = json.dumps(obj)
return val.encode("utf-8")
md.set_xattr(
"osxphotos.metadata:detected_text", detected_text, encode=encoder
)
return detected_text
frozen.detected_text = detected_text
frozen._detected_text = _detected_text
return frozen
def _add_exiftool(
frozen: SimpleNamespace, photo: "osxphotos.photoinfo.PhotoInfo"
) -> SimpleNamespace:
"""Add exiftool property to frozen PhotoInfo object"""
frozen._exiftool_path = photo._db._exiftool_path or None
return frozen
def _add_render_template(frozen: SimpleNamespace) -> SimpleNamespace:
"""Add render_template method to frozen PhotoInfo object"""
def render_template(template_str: str, options: RenderOptions | None = None):
"""Renders a template string for PhotoInfo instance using PhotoTemplate
Args:
template_str: a template string with fields to render
options: a RenderOptions instance
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
"""
options = options or RenderOptions()
template = PhotoTemplate(frozen, exiftool_path=frozen._exiftool_path)
return template.render(template_str, options)
frozen.render_template = render_template
return frozen

View File

@@ -11,7 +11,7 @@ import photoscript
from strpdatetime import strpdatetime
from tenacity import retry, stop_after_attempt, wait_exponential
from ._constants import _DB_TABLE_NAMES
from ._constants import _DB_TABLE_NAMES, SQLITE_CHECK_SAME_THREAD
from .datetime_utils import (
datetime_has_tz,
datetime_remove_tz,
@@ -219,7 +219,7 @@ def _set_date_added(library_path: str, uuid: str, date_added: datetime.datetime)
asset_table = _DB_TABLE_NAMES[photos_version]["ASSET"]
timestamp = datetime_to_photos_timestamp(date_added)
conn = sqlite3.connect(db_path)
conn = sqlite3.connect(db_path, check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
c.execute(
f"UPDATE {asset_table} SET ZADDEDDATE=? WHERE ZUUID=?",
@@ -268,7 +268,7 @@ def get_photo_date_added(
photos_version = get_photos_library_version(library_path)
db_path = str(pathlib.Path(library_path) / "database/Photos.sqlite")
asset_table = _DB_TABLE_NAMES[photos_version]["ASSET"]
conn = sqlite3.connect(db_path)
conn = sqlite3.connect(db_path, check_same_thread=SQLITE_CHECK_SAME_THREAD)
c = conn.cursor()
c.execute(
f"SELECT ZADDEDDATE FROM {asset_table} WHERE ZUUID=?",

View File

@@ -12,6 +12,7 @@ from collections import namedtuple # pylint: disable=syntax-error
from dataclasses import asdict, dataclass
from datetime import datetime
from enum import Enum
from types import SimpleNamespace
import photoscript
from mako.template import Template
@@ -31,7 +32,7 @@ from ._constants import (
)
from ._version import __version__
from .datetime_utils import datetime_tz_to_utc
from .exiftool import ExifTool, exiftool_can_write
from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftool_path
from .export_db import ExportDB, ExportDBTemp
from .fileutil import FileUtil
from .photokit import (
@@ -68,6 +69,7 @@ if t.TYPE_CHECKING:
# retry if download_missing/use_photos_export fails the first time (which sometimes it does)
MAX_PHOTOSCRIPT_RETRIES = 3
# return values for _should_update_photo
class ShouldUpdate(Enum):
NOT_IN_DATABASE = 1
@@ -309,7 +311,6 @@ class ExportResults:
xattr_skipped=None,
xattr_written=None,
):
local_vars = locals()
self._datetime = datetime.now().isoformat()
for attr in self.attributes:
@@ -374,7 +375,7 @@ class PhotoExporter:
def __init__(self, photo: "PhotoInfo", tmpdir: t.Optional[str] = None):
self.photo = photo
self._render_options = RenderOptions()
self._verbose = self.photo._verbose
self._verbose = photo._verbose
# define functions for adding markup
self._filepath = add_rich_markup_tag("filepath", rich=False)
@@ -950,7 +951,8 @@ class PhotoExporter:
"""Stage a photo for export with AppleScript to a temporary directory
Note: If exporting an edited live photo, the associated live video will not be exported.
This is a limitation of the Photos AppleScript interface and Photos behaves the same way."""
This is a limitation of the Photos AppleScript interface and Photos behaves the same way.
"""
if options.edited and not self.photo.hasadjustments:
raise ValueError("Edited version requested but photo has no adjustments")
@@ -1564,7 +1566,7 @@ class PhotoExporter:
with ExifTool(
filepath,
flags=options.exiftool_flags,
exiftool=self.photo._db._exiftool_path,
exiftool=self.photo._exiftool_path,
) as exiftool:
for exiftag, val in exif_info.items():
if type(val) == list:
@@ -1744,7 +1746,6 @@ class PhotoExporter:
elif self.photo.ismovie:
exif["Keys:GPSCoordinates"] = f"{lat} {lon}"
exif["UserData:GPSCoordinates"] = f"{lat} {lon}"
# process date/time and timezone offset
# Photos exports the following fields and sets modify date to creation date
# [EXIF] Modify Date : 2020:10:30 00:00:00
@@ -1854,7 +1855,7 @@ class PhotoExporter:
def _get_exif_keywords(self):
"""returns list of keywords found in the file's exif metadata"""
keywords = []
exif = self.photo.exiftool
exif = exiftool_caching(self.photo)
if exif:
exifdict = exif.asdict()
for field in ["IPTC:Keywords", "XMP:TagsList", "XMP:Subject"]:
@@ -1871,7 +1872,7 @@ class PhotoExporter:
def _get_exif_persons(self):
"""returns list of persons found in the file's exif metadata"""
persons = []
exif = self.photo.exiftool
exif = exiftool_caching(self.photo)
if exif:
exifdict = exif.asdict()
try:
@@ -2142,3 +2143,32 @@ def rename_jpeg_files(files, jpeg_ext, fileutil):
else:
new_files.append(file)
return new_files
def exiftool_caching(photo: SimpleNamespace) -> ExifToolCaching:
"""Return ExifToolCaching object for photo
Args:
photo: SimpleNamespace object with photo info
Returns:
ExifToolCaching object
"""
try:
return photo._exiftool_caching
except AttributeError:
try:
exiftool_path = photo._exiftool_path or get_exiftool_path()
if photo.path is not None and os.path.isfile(photo.path):
exiftool = ExifToolCaching(photo.path, exiftool=exiftool_path)
else:
exiftool = None
except FileNotFoundError:
# get_exiftool_path raises FileNotFoundError if exiftool not found
exiftool = None
logging.warning(
"exiftool not in path; download and install from https://exiftool.org/"
)
photo._exiftool_caching = exiftool
return photo._exiftool_caching

View File

@@ -8,14 +8,16 @@ import contextlib
import dataclasses
import datetime
import json
import logging
import os
import os.path
import pathlib
import plistlib
import re
from datetime import timedelta, timezone
from functools import cached_property
from types import SimpleNamespace
from typing import Any, Dict, Optional
import logging
import yaml
from osxmetadata import OSXMetaData
@@ -66,7 +68,7 @@ from .text_detection import detect_text
from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, hexdigest, list_directory
__all__ = ["PhotoInfo", "PhotoInfoNone"]
__all__ = ["PhotoInfo", "PhotoInfoNone", "frozen_photoinfo_factory"]
logger = logging.getLogger("osxphotos")
@@ -81,6 +83,7 @@ class PhotoInfo:
self._uuid: str = uuid
self._info: dict[str, Any] = info
self._db: "osxphotos.PhotosDB" = db
self._exiftool_path = self._db._exiftool_path
self._verbose = self._db._verbose
@property
@@ -388,6 +391,8 @@ class PhotoInfo:
"""return path_edited_live_photo for Photos <= 4"""
if self._db._db_version > _PHOTOS_4_VERSION:
raise RuntimeError("Wrong database format!")
if not self.live_photo:
return None
photopath = self._get_predicted_path_edited_live_photo_4()
if photopath is not None and not os.path.isfile(photopath):
# the heuristic failed, so try to find the file
@@ -401,10 +406,6 @@ class PhotoInfo:
),
None,
)
if photopath is None:
logger.debug(
f"MISSING PATH: edited live photo file for UUID {self._uuid} does not appear to exist"
)
return photopath
def _path_edited_5_live_photo(self):
@@ -1198,7 +1199,6 @@ class PhotoInfo:
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logger.debug(f"score not implemented for this database version")
return None
try:
@@ -1344,7 +1344,6 @@ class PhotoInfo:
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logger.debug(f"exif_info not implemented for this database version")
return None
try:
@@ -1427,11 +1426,14 @@ class PhotoInfo:
def hexdigest(self):
"""Returns a unique digest of the photo's properties and metadata;
useful for detecting changes in any property/metadata of the photo"""
return hexdigest(self.json())
return hexdigest(self._json_hexdigest())
@cached_property
def cloud_metadata(self) -> Dict:
"""Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict"""
def cloud_metadata(self) -> dict[Any, Any]:
"""Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict; Photos 5+ only"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return {}
# This is a large blob of data so don't load it unless requested
asset_table = _DB_TABLE_NAMES[self._db._photos_ver]["ASSET"]
sql_cloud_metadata = f"""
@@ -1442,10 +1444,6 @@ class PhotoInfo:
WHERE {asset_table}.ZUUID = ?
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logger.debug(f"cloud_metadata not implemented for this database version")
return {}
_, cursor = self._db.get_db_connection()
metadata = {}
if results := cursor.execute(sql_cloud_metadata, (self.uuid,)).fetchone():
@@ -1782,80 +1780,112 @@ class PhotoInfo:
def asdict(self):
"""return dict representation"""
folders = {album.title: album.folder_names for album in self.album_info}
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
place = self.place.asdict() if self.place else {}
score = dataclasses.asdict(self.score) if self.score else {}
adjustments = self.adjustments.asdict() if self.adjustments else {}
album_info = [album.asdict() for album in self.album_info]
burst_album_info = [a.asdict() for a in self.burst_album_info]
burst_photos = [p.uuid for p in self.burst_photos]
comments = [comment.asdict() for comment in self.comments]
exif_info = dataclasses.asdict(self.exif_info) if self.exif_info else {}
face_info = [face.asdict() for face in self.face_info]
folders = {album.title: album.folder_names for album in self.album_info}
import_info = self.import_info.asdict() if self.import_info else {}
likes = [like.asdict() for like in self.likes]
faces = [face.asdict() for face in self.face_info]
person_info = [p.asdict() for p in self.person_info]
place = self.place.asdict() if self.place else {}
project_info = [p.asdict() for p in self.project_info]
score = dataclasses.asdict(self.score) if self.score else {}
search_info = self.search_info.asdict() if self.search_info else {}
search_info_normalized = (
self.search_info_normalized.asdict() if self.search_info_normalized else {}
)
return {
"library": self._db._library_path,
"uuid": self.uuid,
"filename": self.filename,
"original_filename": self.original_filename,
"adjustments": adjustments,
"album_info": album_info,
"albums": self.albums,
"burst_album_info": burst_album_info,
"burst_albums": self.burst_albums,
"burst_default_pick": self.burst_default_pick,
"burst_key": self.burst_key,
"burst_photos": burst_photos,
"burst_selected": self.burst_selected,
"burst": self.burst,
"cloud_guid": self.cloud_guid,
"cloud_metadata": self.cloud_metadata,
"cloud_owner_hashed_id": self.cloud_owner_hashed_id,
"comments": comments,
"date_added": self.date_added,
"date_modified": self.date_modified,
"date_trashed": self.date_trashed,
"date": self.date,
"description": self.description,
"title": self.title,
"keywords": self.keywords,
"labels": self.labels,
"keywords": self.keywords,
"albums": self.albums,
"folders": folders,
"persons": self.persons,
"faces": faces,
"path": self.path,
"ismissing": self.ismissing,
"hasadjustments": self.hasadjustments,
"exif_info": exif_info,
"external_edit": self.external_edit,
"face_info": face_info,
"favorite": self.favorite,
"filename": self.filename,
"fingerprint": self.fingerprint,
"folders": folders,
"has_raw": self.has_raw,
"hasadjustments": self.hasadjustments,
"hdr": self.hdr,
"height": self.height,
"hidden": self.hidden,
"latitude": self._latitude,
"longitude": self._longitude,
"path_edited": self.path_edited,
"shared": self.shared,
"isphoto": self.isphoto,
"ismovie": self.ismovie,
"uti": self.uti,
"uti_original": self.uti_original,
"burst": self.burst,
"live_photo": self.live_photo,
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"import_info": import_info,
"incloud": self.incloud,
"intrash": self.intrash,
"iscloudasset": self.iscloudasset,
"ismissing": self.ismissing,
"ismovie": self.ismovie,
"isphoto": self.isphoto,
"israw": self.israw,
"isreference": self.isreference,
"date_modified": self.date_modified,
"keywords": self.keywords,
"labels_normalized": self.labels_normalized,
"labels": self.labels,
"latitude": self._latitude,
"library": self._db._library_path,
"likes": likes,
"live_photo": self.live_photo,
"location": self.location,
"longitude": self._longitude,
"orientation": self.orientation,
"original_filename": self.original_filename,
"original_filesize": self.original_filesize,
"original_height": self.original_height,
"original_orientation": self.original_orientation,
"original_width": self.original_width,
"owner": self.owner,
"panorama": self.panorama,
"path_derivatives": self.path_derivatives,
"path_edited_live_photo": self.path_edited_live_photo,
"path_edited": self.path_edited,
"path_live_photo": self.path_live_photo,
"path_raw": self.path_raw,
"path": self.path,
"person_info": person_info,
"persons": self.persons,
"place": place,
"portrait": self.portrait,
"project_info": project_info,
"raw_original": self.raw_original,
"score": score,
"screenshot": self.screenshot,
"search_info_normalized": search_info_normalized,
"search_info": search_info,
"selfie": self.selfie,
"shared": self.shared,
"slow_mo": self.slow_mo,
"time_lapse": self.time_lapse,
"hdr": self.hdr,
"selfie": self.selfie,
"panorama": self.panorama,
"has_raw": self.has_raw,
"israw": self.israw,
"raw_original": self.raw_original,
"title": self.title,
"tzoffset": self.tzoffset,
"uti_edited": self.uti_edited,
"uti_original": self.uti_original,
"uti_raw": self.uti_raw,
"path_raw": self.path_raw,
"place": place,
"exif": exif,
"score": score,
"intrash": self.intrash,
"height": self.height,
"uti": self.uti,
"uuid": self.uuid,
"visible": self.visible,
"width": self.width,
"orientation": self.orientation,
"original_height": self.original_height,
"original_width": self.original_width,
"original_orientation": self.original_orientation,
"original_filesize": self.original_filesize,
"comments": comments,
"likes": likes,
"search_info": search_info,
"fingerprint": self.fingerprint,
"cloud_guid": self.cloud_guid,
"cloud_owner_hashed_id": self.cloud_owner_hashed_id,
}
def json(self):
@@ -1867,8 +1897,44 @@ class PhotoInfo:
dict_data = self.asdict()
for k, v in dict_data.items():
# sort lists such as keywords so JSON is consistent
# but do not sort certain items like location
if k in ["location"]:
continue
if v and isinstance(v, (list, tuple)) and not isinstance(v[0], dict):
dict_data[k] = sorted(v)
dict_data[k] = sorted(v, key=lambda v: v if v is not None else "")
return json.dumps(dict_data, sort_keys=True, default=default)
def _json_hexdigest(self):
"""JSON for use by hexdigest()"""
# This differs from json() because hexdigest must not change if metadata changed
# With json(), sort order of lists of dicts is not consistent but these aren't needed
# for computing hexdigest so we can ignore them
# also don't use visible because it changes based on Photos UI state
def default(o):
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()
dict_data = self.asdict()
for k in [
"album_info",
"burst_album_info",
"face_info",
"person_info",
"visible",
]:
del dict_data[k]
for k, v in dict_data.items():
# sort lists such as keywords so JSON is consistent
# but do not sort certain items like location
if k in ["location"]:
continue
if v and isinstance(v, (list, tuple)) and not isinstance(v[0], dict):
dict_data[k] = sorted(v, key=lambda v: v if v is not None else "")
return json.dumps(dict_data, sort_keys=True, default=default)
def __eq__(self, other):
@@ -1893,10 +1959,111 @@ class PhotoInfo:
class PhotoInfoNone:
"""mock class that returns None for all attributes"""
"""Mock class that returns None for all attributes"""
def __init__(self):
pass
def __getattribute__(self, name):
return None
def frozen_photoinfo_factory(photo: PhotoInfo) -> SimpleNamespace:
"""Return a frozen SimpleNamespace object for a PhotoInfo object"""
photo_json = photo.json()
def _object_hook(d: dict[Any, Any]):
if not d:
return d
# if d key matches a ISO 8601 datetime ('2023-03-24T06:46:57.690786', '2019-07-04T16:24:01-07:00', '2019-07-04T16:24:01+07:00'), convert to datetime
# fromisoformat will also handle dates with timezone offset in form +0700, etc.
for k, v in d.items():
if isinstance(v, str) and re.match(
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]?\d*[+-]?\d{2}[:]?\d{2}?", v
):
d[k] = datetime.datetime.fromisoformat(v)
return SimpleNamespace(**d)
frozen = json.loads(photo_json, object_hook=lambda d: _object_hook(d))
# add on json() method to frozen object
def _json(*args):
return photo_json
frozen.json = _json
# add hexdigest property to frozen object
frozen.hexdigest = photo.hexdigest
def detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)
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
If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available
Returns: list of (detected text, confidence) tuples
"""
try:
return frozen._detected_text_cache[confidence_threshold]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
frozen._detected_text_cache = {}
try:
detected_text = frozen._detected_text()
except Exception as e:
logging.warning(f"Error detecting text in photo {frozen.uuid}: {e}")
detected_text = []
frozen._detected_text_cache[confidence_threshold] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return frozen._detected_text_cache[confidence_threshold]
def _detected_text():
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
path = (
frozen.path_edited
if frozen.hasadjustments and frozen.path_edited
else frozen.path
)
path = path or frozen.path_derivatives[0] if frozen.path_derivatives else None
if not path:
return []
md = OSXMetaData(path)
try:
def decoder(val):
"""Decode value from JSON"""
return json.loads(val.decode("utf-8"))
detected_text = md.get_xattr(
"osxphotos.metadata:detected_text", decode=decoder
)
except KeyError:
detected_text = None
if detected_text is None:
orientation = frozen.orientation or None
detected_text = detect_text(path, orientation)
def encoder(obj):
"""Encode value as JSON"""
val = json.dumps(obj)
return val.encode("utf-8")
md.set_xattr(
"osxphotos.metadata:detected_text", detected_text, encode=encoder
)
return detected_text
frozen.detected_text = detected_text
frozen._detected_text = _detected_text
return frozen

View File

@@ -284,6 +284,9 @@ class PhotosDB:
# key is Z_PK of ZMOMENT table and values are the moment info
self._db_moment_pk = {}
# Dict to hold data on imports for Photos <= 4
self._db_import_group = {}
logger.debug(f"dbfile = {dbfile}")
if dbfile is None:

View File

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

View File

@@ -11,7 +11,7 @@ from typing import Callable, Optional, Tuple
from photoscript import Photo
from tenacity import retry, stop_after_attempt, wait_exponential
from ._constants import _DB_TABLE_NAMES
from ._constants import _DB_TABLE_NAMES, SQLITE_CHECK_SAME_THREAD
from .photosdb.photosdb_utils import get_photos_library_version
from .timezones import Timezone
from .utils import get_last_library_path, get_system_library_path, noop
@@ -67,7 +67,9 @@ class PhotoTimeZone:
ON ZADDITIONALASSETATTRIBUTES.ZASSET = {self.ASSET_TABLE}.Z_PK
WHERE {self.ASSET_TABLE}.ZUUID = '{uuid}'
"""
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(
self.db_path, check_same_thread=SQLITE_CHECK_SAME_THREAD
) as conn:
c = conn.cursor()
c.execute(sql)
results = c.fetchone()
@@ -137,7 +139,9 @@ class PhotoTimeZoneUpdater:
ON ZADDITIONALASSETATTRIBUTES.ZASSET = {self.ASSET_TABLE}.Z_PK
WHERE {self.ASSET_TABLE}.ZUUID = '{uuid}'
"""
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(
self.db_path, check_same_thread=SQLITE_CHECK_SAME_THREAD
) as conn:
c = conn.cursor()
c.execute(sql)
results = c.fetchone()
@@ -151,7 +155,9 @@ class PhotoTimeZoneUpdater:
ZTIMEZONENAME='{self.tz_name}'
WHERE Z_PK={z_pk};
"""
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(
self.db_path, check_same_thread=SQLITE_CHECK_SAME_THREAD
) as conn:
c = conn.cursor()
c.execute(sql_update)
conn.commit()

View File

@@ -5,14 +5,22 @@ import pathlib
import sqlite3
from typing import List, Tuple
from ._constants import SQLITE_CHECK_SAME_THREAD
logger = logging.getLogger("osxphotos")
def sqlite_open_ro(dbname: str) -> Tuple[sqlite3.Connection, sqlite3.Cursor]:
"""opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor)"""
try:
dbpath = pathlib.Path(dbname).resolve()
conn = sqlite3.connect(f"{dbpath.as_uri()}?mode=ro", timeout=1, uri=True)
conn = sqlite3.connect(
f"{dbpath.as_uri()}?mode=ro",
timeout=1,
uri=True,
check_same_thread=SQLITE_CHECK_SAME_THREAD,
)
c = conn.cursor()
except sqlite3.Error as e:
raise sqlite3.Error(

View File

@@ -9,6 +9,8 @@ from typing import Callable, Dict, Generator, Iterable, Optional, Tuple, TypeVar
# keep mypy happy, keys/values can be any type supported by SQLite
T = TypeVar("T")
__version__ = "0.3.0"
__all__ = ["SQLiteKVStore"]
@@ -41,7 +43,7 @@ class SQLiteKVStore:
self._serialize_func = serialize
self._deserialize_func = deserialize
self._conn = (
sqlite3.Connection(dbpath)
sqlite3.connect(dbpath)
if os.path.exists(dbpath)
else self._create_database(dbpath)
)
@@ -53,7 +55,7 @@ class SQLiteKVStore:
def _create_database(self, dbpath: str):
"""Create the key-value database"""
conn = sqlite3.Connection(dbpath)
conn = sqlite3.connect(dbpath)
cursor = conn.cursor()
cursor.execute(
"""CREATE TABLE IF NOT EXISTS _about (

View File

@@ -4631,7 +4631,7 @@ def test_export_force_update():
conn = sqlite3.connect(dbpath)
c = conn.cursor()
except sqlite3.Error as e:
pytest.exit(f"An error occurred opening sqlite file")
pytest.exit("An error occurred opening sqlite file")
# photo is IMG_4547.jpg
c.execute(

View File

@@ -0,0 +1,66 @@
""""Test that PhotoInfo.export can export concurrently"""
import concurrent.futures
import pathlib
import sqlite3
import tempfile
import pytest
import osxphotos
PHOTOS_DB = "tests/Test-10.15.7.photoslibrary"
@pytest.mark.skipif(sqlite3.threadsafety != 3, reason="sqlite3 not threadsafe")
@pytest.mark.parametrize(
"count", range(10)
) # repeat multiple times to try to catch any concurrency errors
def test_concurrent_export(count):
"""Test that PhotoInfo.export can export concurrently"""
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = [p for p in photosdb.photos() if not p.ismissing]
with tempfile.TemporaryDirectory() as tmpdir:
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = [
executor.submit(p.export, tmpdir, f"{p.uuid}_{p.original_filename}")
for p in photos
]
exported = []
for future in concurrent.futures.as_completed(futures):
exported.extend(future.result())
assert len(exported) == len(photos)
@pytest.mark.skipif(sqlite3.threadsafety != 3, reason="sqlite3 not threadsafe")
@pytest.mark.parametrize(
"count", range(10)
) # repeat multiple times to try to catch any concurrency errors
def test_concurrent_export_with_exportdb(count):
"""Test that PhotoInfo.export can export concurrently"""
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = [p for p in photosdb.photos() if not p.ismissing]
with tempfile.TemporaryDirectory() as tmpdir:
exportdb = osxphotos.ExportDB(pathlib.Path(tmpdir) / "export.db", tmpdir)
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = []
for p in photos:
options = osxphotos.ExportOptions()
options.export_db = exportdb
exporter = osxphotos.PhotoExporter(p)
futures.append(
executor.submit(
exporter.export,
tmpdir,
f"{p.uuid}_{p.original_filename}",
options=options,
)
)
export_results = osxphotos.photoexporter.ExportResults()
for future in concurrent.futures.as_completed(futures):
export_results += future.result()
assert len(export_results.exported) == len(photos)
assert len(list(exportdb.get_exported_files())) == len(photos)

View File

@@ -419,22 +419,6 @@ def test_addvalues_unicode():
assert sorted(exif.data["IPTC:Keywords"]) == sorted(["ǂ", "Ƕ"])
def test_singleton():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_MULTI_KEYWORD)
assert exif1._process.pid == exif2._process.pid
def test_pid():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif1.pid == exif1._process.pid
def test_exiftoolproc_process():
import osxphotos.exiftool