Feature timewarp (#675)

* Implemented timewarp command

* Updated docs

* Added missing pytest mark
This commit is contained in:
Rhet Turnbull 2022-05-01 10:01:05 -07:00 committed by GitHub
parent 8a3dc9b393
commit dc4d322dab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 3857 additions and 77 deletions

View File

@ -124,9 +124,10 @@ This package will install a command line utility called `osxphotos` that allows
```
> osxphotos
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
osxphotos: query and export your Photos library
Options:
--db <Photos database path> Specify Photos database path. Path to Photos
library/database can be specified using either
@ -144,8 +145,8 @@ Commands:
about Print information about osxphotos including license.
albums Print out albums found in the Photos library.
diff Compare two Photos databases and print out differences
dump Print list of all photos & associated info from the Photos...
docs Open osxphotos documentation in your browser.
dump Print list of all photos & associated info from the Photos...
export Export photos from the Photos database.
help Print help; for help on commands: help <command>.
info Print out descriptive info of the Photos library database.
@ -157,7 +158,10 @@ Commands:
places Print out places found in the Photos library.
query Query the Photos database using 1 or more search options; if...
repl Run interactive osxphotos REPL shell (useful for debugging,...
run Run a python file using same environment as osxphotos
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos

View File

@ -95,6 +95,7 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
-v, --version Show the version and exit.
-h, --help Show this message and exit.
Commands:
about Print information about osxphotos including license.
albums Print out albums found in the Photos library.
@ -115,6 +116,7 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
run Run a python file using same environment as osxphotos
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos

View File

@ -74,6 +74,7 @@ This package will install a command line utility called ``osxphotos`` that allow
run Run a python file using same environment as osxphotos
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos

View File

@ -2110,6 +2110,118 @@ uses /private/tmp/osxphotos_snapshots</p>
<dd><p>Delete THEME.</p>
</dd></dl>
</section>
<section id="osxphotos-timewarp">
<h3>timewarp<a class="headerlink" href="#osxphotos-timewarp" title="Permalink to this headline">#</a></h3>
<p>Adjust date/time/timezone of photos in Apple Photos.</p>
<p>Changes will be applied to all photos currently selected in Photos.
timewarp cannot operate on photos selected in a Smart Album;
select photos in a regular album or in the All Photos view.
See Timewarp Overview below for additional information.</p>
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>osxphotos timewarp <span class="o">[</span>OPTIONS<span class="o">]</span>
</pre></div>
</div>
<p class="rubric">Options</p>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-d">
<span id="cmdoption-osxphotos-timewarp-date"></span><span class="sig-name descname"><span class="pre">-d</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--date</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;DATE&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-d" title="Permalink to this definition">#</a></dt>
<dd><p>Set date for selected photos. Format is YYYY-MM-DD.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-D">
<span id="cmdoption-osxphotos-timewarp-date-delta"></span><span class="sig-name descname"><span class="pre">-D</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--date-delta</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;DELTA&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-D" title="Permalink to this definition">#</a></dt>
<dd><p>Adjust date for selected photos by DELTA. Format is one of: ±D days, ±W weeks, ±D where D is days</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-t">
<span id="cmdoption-osxphotos-timewarp-time"></span><span class="sig-name descname"><span class="pre">-t</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--time</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;TIME&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-t" title="Permalink to this definition">#</a></dt>
<dd><p>Set time for selected photos. Format is one of HH:MM:SS, HH:MM:SS.fff, HH:MM.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-T">
<span id="cmdoption-osxphotos-timewarp-time-delta"></span><span class="sig-name descname"><span class="pre">-T</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--time-delta</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;DELTA&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-T" title="Permalink to this definition">#</a></dt>
<dd><p>Adjust time for selected photos by DELTA time. Format is one of ±HH:MM:SS, ±H hours (or hr), ±M minutes (or min), ±S seconds (or sec), ±S (where S is seconds)</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-z">
<span id="cmdoption-osxphotos-timewarp-timezone"></span><span class="sig-name descname"><span class="pre">-z</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--timezone</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;TIMEZONE&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-z" title="Permalink to this definition">#</a></dt>
<dd><p>Set timezone for selected photos as offset from UTC. Format is one of ±HH:MM, ±H:MM, or ±HHMM. The actual time of the photo is not adjusted which means, somewhat counterintuitively, that the time in the new timezone will be different. For example, if photo has time of 12:00 and timezone of GMT+01:00 and new timezone is specified as timezone +02:00 (one hour ahead of current GMT+01:00 timezone), the photos new time will be 13:00 GMT+02:00, which is equivalent to the old time of 12:00+01:00. This is the same behavior exhibited by Photos when manually adjusting timezone in the Get Info window. See also match-time.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-i">
<span id="cmdoption-osxphotos-timewarp-inspect"></span><span class="sig-name descname"><span class="pre">-i</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--inspect</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-i" title="Permalink to this definition">#</a></dt>
<dd><p>Print out the date/time/timezone for each selected photo without changing any information.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-c">
<span id="cmdoption-osxphotos-timewarp-compare-exif"></span><span class="sig-name descname"><span class="pre">-c</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--compare-exif</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-c" title="Permalink to this definition">#</a></dt>
<dd><p>Compare the EXIF date/time/timezone for each selected photo to the same data in Photos. Requires the third-party exiftool utility be installed (see <a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>). See also add-to-album.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-p">
<span id="cmdoption-osxphotos-timewarp-push-exif"></span><span class="sig-name descname"><span class="pre">-p</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--push-exif</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-p" title="Permalink to this definition">#</a></dt>
<dd><p>Push date/time and timezone for selected photos from Photos to the EXIF metadata in the original file in the Photos library. Requires the third-party exiftool utility be installed (see <a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>). Using this option modifies the <em>original</em> file of the image in your Photos library. push-exif will be executed after any other updates are performed on the photo. See also pull-exif.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-P">
<span id="cmdoption-osxphotos-timewarp-pull-exif"></span><span class="sig-name descname"><span class="pre">-P</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--pull-exif</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-P" title="Permalink to this definition">#</a></dt>
<dd><p>Pull date/time and timezone for selected photos from EXIF metadata in the original file into Photos and update the associated data in Photos to match the EXIF data. pull-exif will be executed before any other updates are performed on the photo. It is possible for images to have missing EXIF data, for example the date/time could be set but there might be no timezone set in the EXIF metadata. Missing data will be handled thusly: if date/time/timezone are all present in the EXIF data, the photos date/time/timezone will be updated. If timezone is missing but date/time is present, only the photos date/time will be updated. If date/time is missing but the timezone is present, only the photos timezone will be updated unless use-file-time is set in which case, the photos file modification date/time will be used in place of EXIF date/time. If the date is present but the time is missing, the time will be set to 00:00:00. Requires the third-party exiftool utility be installed (see <a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>). See also push-exif.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-m">
<span id="cmdoption-osxphotos-timewarp-match-time"></span><span class="sig-name descname"><span class="pre">-m</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--match-time</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-m" title="Permalink to this definition">#</a></dt>
<dd><p>When used with timezone, adjusts the photo time so that the timestamp in the new timezone matches the timestamp in the old timezone. For example, if photo has time of 12:00 and timezone of GMT+01:00 and new timezone is specified as timezone +02:00 (one hour ahead of current GMT+01:00 timezone), the photos new time will be 12:00 GMT+02:00. That is, the timezone will have changed but the timestamp of the photo will match the previous timestamp. Use match-time when the cameras time was correct for the time the photo was taken but the timezone was missing or wrong and you want to adjust the timezone while preserving the photos time. See also timezone.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-f">
<span id="cmdoption-osxphotos-timewarp-use-file-time"></span><span class="sig-name descname"><span class="pre">-f</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--use-file-time</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-f" title="Permalink to this definition">#</a></dt>
<dd><p>When used with pull-exif, the file modification date/time will be used if date/time is missing from the EXIF data.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-a">
<span id="cmdoption-osxphotos-timewarp-add-to-album"></span><span class="sig-name descname"><span class="pre">-a</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--add-to-album</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;ALBUM&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-a" title="Permalink to this definition">#</a></dt>
<dd><p>When used with compare-exif, adds any photos with date/time/timezone differences between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-V">
<span id="cmdoption-osxphotos-timewarp-v"></span><span id="cmdoption-osxphotos-timewarp-verbose"></span><span class="sig-name descname"><span class="pre">-V</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--verbose</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-V" title="Permalink to this definition">#</a></dt>
<dd><p>Show verbose output.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-L">
<span id="cmdoption-osxphotos-timewarp-l"></span><span id="cmdoption-osxphotos-timewarp-library"></span><span class="sig-name descname"><span class="pre">-L</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--library</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;PHOTOS_LIBRARY_PATH&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-L" title="Permalink to this definition">#</a></dt>
<dd><p>Path to Photos library (e.g. ~/Pictures/PhotosLibrary.photoslibrary). This is not likely needed as osxphotos will usually be able to locate the path to the open Photos library. Use library only if you get an error that the Photos library cannot be located.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-e">
<span id="cmdoption-osxphotos-timewarp-exiftool-path"></span><span class="sig-name descname"><span class="pre">-e</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--exiftool-path</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;exiftool_path&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-e" title="Permalink to this definition">#</a></dt>
<dd><p>Optional path to exiftool executable (will look in $PATH if not specified) for those options which require exiftool.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-o">
<span id="cmdoption-osxphotos-timewarp-output-file"></span><span class="sig-name descname"><span class="pre">-o</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--output-file</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;output_file&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-o" title="Permalink to this definition">#</a></dt>
<dd><p>Output file. If not specified, output is written to stdout.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-timestamp">
<span class="sig-name descname"><span class="pre">--timestamp</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-timestamp" title="Permalink to this definition">#</a></dt>
<dd><p>Add time stamp to verbose output</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-theme">
<span class="sig-name descname"><span class="pre">--theme</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;THEME&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-theme" title="Permalink to this definition">#</a></dt>
<dd><p>Specify the color theme to use for verbose output. Valid themes are dark, light, mono, and plain. Defaults to dark or light depending on system dark mode setting.</p>
<dl class="field-list simple">
<dt class="field-odd">Options</dt>
<dd class="field-odd"><p>dark | light | mono | plain</p>
</dd>
</dl>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-timewarp-plain">
<span class="sig-name descname"><span class="pre">--plain</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-timewarp-plain" title="Permalink to this definition">#</a></dt>
<dd><p>Plain text mode. Do not use rich output.</p>
</dd></dl>
</section>
<section id="osxphotos-tutorial">
<h3>tutorial<a class="headerlink" href="#osxphotos-tutorial" title="Permalink to this headline">#</a></h3>
<p>Display osxphotos tutorial.</p>
@ -2289,6 +2401,7 @@ Commands:
<li><a class="reference internal" href="#osxphotos-run">run</a></li>
<li><a class="reference internal" href="#osxphotos-snap">snap</a></li>
<li><a class="reference internal" href="#osxphotos-theme">theme</a></li>
<li><a class="reference internal" href="#osxphotos-timewarp">timewarp</a></li>
<li><a class="reference internal" href="#osxphotos-tutorial">tutorial</a></li>
<li><a class="reference internal" href="#osxphotos-uninstall">uninstall</a></li>
<li><a class="reference internal" href="#osxphotos-uuid">uuid</a></li>

View File

@ -227,6 +227,8 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-query-add-to-album">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-a">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -279,6 +281,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-cloudasset">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-cloudasset">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--compare-exif
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-c">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -307,6 +316,20 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-current-name">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--date
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-d">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--date-delta
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-D">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -506,6 +529,8 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-exiftool-path">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-e">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -712,6 +737,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-incloud">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-incloud">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--inspect
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-i">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -791,6 +823,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-label">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-label">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--library
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-L">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -827,6 +866,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-location">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-location">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--match-time
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-m">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -946,8 +992,6 @@
<li><a href="cli.html#cmdoption-osxphotos-repl-no-title">osxphotos-repl command line option</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
--not-burst
@ -990,6 +1034,8 @@
<li><a href="cli.html#cmdoption-osxphotos-repl-not-hdr">osxphotos-repl command line option</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
--not-hidden
@ -1152,6 +1198,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-original-suffix">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--output-file
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-o">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1199,6 +1252,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-place">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-place">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--plain
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-plain">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1247,6 +1307,20 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-preview-suffix">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--pull-exif
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-P">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--push-exif
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-p">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1468,6 +1542,22 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-theme">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-theme">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--time
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-t">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--time-delta
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-T">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1486,6 +1576,15 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-timestamp">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-timestamp">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
--timezone
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-z">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1547,6 +1646,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-install-U">osxphotos-install command line option</a>
</li>
</ul></li>
<li>
--use-file-time
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-f">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1603,6 +1709,8 @@
<li><a href="cli.html#cmdoption-osxphotos-diff-V">osxphotos-diff command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-V">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-V">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1635,12 +1743,49 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-uninstall-y">osxphotos-uninstall command line option</a>
</li>
</ul></li>
<li>
-a
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-a">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-c
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-c">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-D
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-D">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-d
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-d">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-e
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-e">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-f
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-f">osxphotos-timewarp command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-uuid-f">osxphotos-uuid command line option</a>
</li>
</ul></li>
@ -1653,6 +1798,43 @@
<li><a href="cli.html#cmdoption-osxphotos-query-i">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-i">osxphotos-repl command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-i">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-L
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-L">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-m
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-m">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-o
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-o">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-P
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-P">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-p
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-p">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1667,6 +1849,20 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-diff-s">osxphotos-diff command line option</a>
</li>
</ul></li>
<li>
-T
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-T">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
-t
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-t">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1683,6 +1879,8 @@
<li><a href="cli.html#cmdoption-osxphotos-diff-V">osxphotos-diff command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-V">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-V">osxphotos-timewarp command line option</a>
</li>
</ul></li>
<li>
@ -1697,6 +1895,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-uninstall-y">osxphotos-uninstall command line option</a>
</li>
</ul></li>
<li>
-z
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-z">osxphotos-timewarp command line option</a>
</li>
</ul></li>
</ul></td>
@ -2760,8 +2965,6 @@
<li><a href="cli.html#cmdoption-osxphotos-list-json">--json</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
osxphotos-persons command line option
@ -2949,6 +3152,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-arg-PHOTOS_LIBRARY">PHOTOS_LIBRARY</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
osxphotos-repl command line option
@ -3141,6 +3346,81 @@
<li><a href="cli.html#cmdoption-osxphotos-theme-list">--list</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-theme-preview">--preview</a>
</li>
</ul></li>
<li>
osxphotos-timewarp command line option
<ul>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-a">--add-to-album</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-c">--compare-exif</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-d">--date</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-D">--date-delta</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-e">--exiftool-path</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-i">--inspect</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-L">--library</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-m">--match-time</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-o">--output-file</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-plain">--plain</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-P">--pull-exif</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-p">--push-exif</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-theme">--theme</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-t">--time</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-T">--time-delta</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-timestamp">--timestamp</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-z">--timezone</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-f">--use-file-time</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-V">--verbose</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-a">-a</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-c">-c</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-d">-d</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-D">-D</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-e">-e</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-f">-f</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-i">-i</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-L">-L</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-m">-m</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-o">-o</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-p">-p</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-P">-P</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-t">-t</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-T">-T</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-V">-V</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-z">-z</a>
</li>
</ul></li>
<li>

View File

@ -251,6 +251,7 @@
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-run">run</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-snap">snap</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-theme">theme</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-timewarp">timewarp</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-uninstall">uninstall</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-uuid">uuid</a></li>

Binary file not shown.

View File

@ -260,6 +260,7 @@ E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine
<span class="n">run</span> <span class="n">Run</span> <span class="n">a</span> <span class="n">python</span> <span class="n">file</span> <span class="n">using</span> <span class="n">same</span> <span class="n">environment</span> <span class="k">as</span> <span class="n">osxphotos</span>
<span class="n">snap</span> <span class="n">Create</span> <span class="n">snapshot</span> <span class="n">of</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">to</span> <span class="n">use</span> <span class="k">with</span> <span class="n">diff</span> <span class="n">command</span>
<span class="n">theme</span> <span class="n">Manage</span> <span class="n">osxphotos</span> <span class="n">color</span> <span class="n">themes</span><span class="o">.</span>
<span class="n">timewarp</span> <span class="n">Adjust</span> <span class="n">date</span><span class="o">/</span><span class="n">time</span><span class="o">/</span><span class="n">timezone</span> <span class="n">of</span> <span class="n">photos</span> <span class="ow">in</span> <span class="n">Apple</span> <span class="n">Photos</span><span class="o">.</span>
<span class="n">tutorial</span> <span class="n">Display</span> <span class="n">osxphotos</span> <span class="n">tutorial</span><span class="o">.</span>
<span class="n">uninstall</span> <span class="n">Uninstall</span> <span class="n">Python</span> <span class="n">packages</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">osxphotos</span> <span class="n">environment</span>
<span class="n">uuid</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">unique</span> <span class="n">IDs</span> <span class="p">(</span><span class="n">UUID</span><span class="p">)</span> <span class="n">of</span> <span class="n">photos</span> <span class="n">selected</span> <span class="ow">in</span> <span class="n">Photos</span>

File diff suppressed because one or more lines are too long

View File

@ -74,6 +74,7 @@ This package will install a command line utility called ``osxphotos`` that allow
run Run a python file using same environment as osxphotos
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos

View File

@ -25,6 +25,7 @@ from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .theme import theme
from .timewarp import timewarp
from .tutorial import tutorial
from .uuid import uuid
from .version import version
@ -82,6 +83,7 @@ for command in [
run,
snap,
theme,
timewarp,
tutorial,
uninstall,
uuid,

View File

@ -22,23 +22,26 @@ __all__ = [
THEME_STYLES = [
"bar.back",
"bar.complete",
"bar.finished",
"bar.pulse",
"change",
"color",
"count",
"error",
"filename",
"filepath",
"highlight",
"no_change",
"num",
"time",
"uuid",
"warning",
"bar.back",
"bar.complete",
"bar.finished",
"bar.pulse",
"progress.elapsed",
"progress.percentage",
"progress.remaining",
"time",
"tz",
"uuid",
"warning",
]
COLOR_THEMES = {
@ -48,23 +51,26 @@ COLOR_THEMES = {
tags=["dark"],
styles={
# color pallette from https://github.com/dracula/dracula-theme
"bar.back": Style(color="rgb(68,71,90)"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(80,250,123)"),
"bar.pulse": Style(color="rgb(98,114,164)"),
"change": Style(color="bright_red", bold=True),
"color": Style(color="rgb(248,248,242)"),
"count": Style(color="rgb(139,233,253)"),
"error": Style(color="rgb(255,85,85)", bold=True),
"filename": Style(color="rgb(189,147,249)", bold=True),
"filepath": Style(color="rgb(80,250,123)", bold=True),
"highlight": Style(color="#000000", bgcolor="#d73a49", bold=True),
"no_change": Style(color="bright_green", bold=True),
"num": Style(color="rgb(139,233,253)", bold=True),
"time": Style(color="rgb(139,233,253)", bold=True),
"uuid": Style(color="rgb(255,184,108)"),
"warning": Style(color="rgb(241,250,140)", bold=True),
"bar.back": Style(color="rgb(68,71,90)"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(80,250,123)"),
"bar.pulse": Style(color="rgb(98,114,164)"),
"progress.elapsed": Style(color="rgb(139,233,253)"),
"progress.percentage": Style(color="rgb(255,121,198)"),
"progress.remaining": Style(color="rgb(139,233,253)"),
"time": Style(color="rgb(139,233,253)", bold=True),
"tz": Style(color="bright_cyan", bold=True),
"uuid": Style(color="rgb(255,184,108)"),
"warning": Style(color="rgb(241,250,140)", bold=True),
# "headers": Style(color="rgb(165,194,97)"),
# "options": Style(color="rgb(255,198,109)"),
# "metavar": Style(color="rgb(12,125,157)"),
@ -74,23 +80,26 @@ COLOR_THEMES = {
name="light",
description="Light mode theme",
styles={
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
"bar.pulse": Style(color="rgb(249,38,114)"),
"change": "bold dark_red",
"color": Style(color="#000000"),
"count": Style(color="#005cc5", bold=True),
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
"filename": Style(color="#6f42c1", bold=True),
"filepath": Style(color="#22863a", bold=True),
"highlight": Style(color="#ffffff", bgcolor="#d73a49", bold=True),
"no_change": "bold dark_green",
"num": Style(color="#005cc5", bold=True),
"time": Style(color="#032f62", bold=True),
"uuid": Style(color="#d73a49", bold=True),
"warning": Style(color="#e36209", bold=True, underline=True, italic=True),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
"bar.pulse": Style(color="rgb(249,38,114)"),
"progress.elapsed": Style(color="#032f62", bold=True),
"progress.percentage": Style(color="#6f42c1", bold=True),
"progress.remaining": Style(color="#032f62", bold=True),
"time": Style(color="#032f62", bold=True),
"tz": "bold cyan",
"uuid": Style(color="#d73a49", bold=True),
"warning": Style(color="#e36209", bold=True, underline=True, italic=True),
# "headers": Style(color="rgb(254,212,66)"),
# "options": Style(color="rgb(227,98,9)"),
# "metavar": Style(color="rgb(111,66,193)"),
@ -101,22 +110,25 @@ COLOR_THEMES = {
description="Monochromatic theme",
tags=["mono", "colorblind"],
styles={
"bar.back": "",
"bar.complete": "reverse",
"bar.finished": "bold",
"bar.pulse": "bold",
"change": "reverse",
"count": "bold",
"error": "reverse italic",
"filename": "bold",
"filepath": "bold underline",
"highlight": "reverse italic",
"no_change": "",
"num": "bold",
"time": "bold",
"uuid": "bold",
"warning": "bold italic",
"bar.back": "",
"bar.complete": "reverse",
"bar.finished": "bold",
"bar.pulse": "bold",
"progress.elapsed": "",
"progress.percentage": "bold",
"progress.remaining": "bold",
"time": "bold",
"tz": "",
"uuid": "bold",
"warning": "bold italic",
# "headers": "bold",
# "options": "bold",
# "metavar": "bold",
@ -127,23 +139,26 @@ COLOR_THEMES = {
description="Plain theme with no colors",
tags=["colorblind"],
styles={
"bar.back": "",
"bar.complete": "",
"bar.finished": "",
"bar.pulse": "",
"change": "",
"color": "",
"count": "",
"error": "",
"filename": "",
"filepath": "",
"highlight": "",
"no_change": "",
"num": "",
"time": "",
"uuid": "",
"warning": "",
"bar.back": "",
"bar.complete": "",
"bar.finished": "",
"bar.pulse": "",
"progress.elapsed": "",
"progress.percentage": "",
"progress.remaining": "",
"time": "",
"tz": "",
"uuid": "",
"warning": "",
# "headers": "",
# "options": "",
# "metavar": "",
@ -162,7 +177,9 @@ def get_theme_dir() -> pathlib.Path:
def get_theme_manager() -> ThemeManager:
"""Return theme manager instance"""
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
return ThemeManager(
theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values(), update=True
)
def get_theme(

View File

@ -569,3 +569,4 @@ def check_version():
"to suppress this message and prevent osxphotos from checking for latest version.",
err=True,
)

View File

@ -5,19 +5,26 @@ import pathlib
import bitmath
import click
import pytimeparse
from osxphotos.export_db_utils import export_db_get_version
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.timeutils import time_string_to_datetime, utc_offset_string_to_seconds
from osxphotos.timezones import Timezone
from osxphotos.utils import expand_and_validate_filepath, load_function
__all__ = [
"BitMathSize",
"DateOffset",
"DateTimeISO8601",
"ExportDBType",
"FunctionCall",
"TimeISO8601",
"TemplateString",
"TimeISO8601",
"TimeOffset",
"TimeString",
"UTCOffset",
]
@ -129,3 +136,75 @@ class TemplateString(click.ParamType):
return value
except ValueError as e:
self.fail(e)
class TimeString(click.ParamType):
"""A timestring in format HH:MM:SS, HH:MM:SS.fff, HH:MM"""
name = "TIMESTRING"
def convert(self, value, param, ctx):
try:
return time_string_to_datetime(value)
except ValueError:
self.fail(
f"Invalid time format: {value}. "
"Valid format for time: 'HH:MM:SS', 'HH:MM:SS.fff', 'HH:MM'"
)
class DateOffset(click.ParamType):
"""A date offset string in the format ±D days, ±W weeks, ±Y years, ±D where D is days"""
name = "DATEOFFSET"
def convert(self, value, param, ctx):
offset = pytimeparse.parse(value)
if offset is not None:
offset = offset / 86400
return datetime.timedelta(days=offset)
# could be in format "-1" (negative offset) or "+1" (positive offset)
try:
return datetime.timedelta(days=int(value))
except ValueError:
self.fail(
f"Invalid date offset format: {value}. "
"Valid format for date/time offset: '±D days', '±W weeks', '±D' where D is days "
)
class TimeOffset(click.ParamType):
"""A time offset string in the format [+-]HH:MM[:SS[.fff[fff]]] or +1 days, -2 hours, -18000, etc"""
name = "TIMEOFFSET"
def convert(self, value, param, ctx):
offset = pytimeparse.parse(value)
if offset is not None:
return datetime.timedelta(seconds=offset)
# could be in format "-18000" (negative offset) or "+18000" (positive offset)
try:
return datetime.timedelta(seconds=int(value))
except ValueError:
self.fail(
f"Invalid time offset format: {value}. "
"Valid format for date/time offset: '±HH:MM:SS', '±H hours' (or hr), '±M minutes' (or min), '±S seconds' (or sec), '±S' (where S is seconds)"
)
class UTCOffset(click.ParamType):
"""A UTC offset timezone in format ±[hh]:[mm], ±[h]:[mm], or ±[hh][mm]"""
name = "UTC_OFFSET"
def convert(self, value, param, ctx):
try:
offset_seconds = utc_offset_string_to_seconds(value)
return Timezone(offset_seconds)
except Exception:
self.fail(
f"Invalid timezone format: {value}. "
"Valid format for timezone offset: '±HH:MM', '±H:MM', or '±HHMM'"
)

542
osxphotos/cli/timewarp.py Normal file
View File

@ -0,0 +1,542 @@
""" Fix time / date / timezone for photos in Apple Photos """
import datetime
import os
import sys
from functools import partial
from textwrap import dedent
from typing import Callable
import click
from photoscript import Photo, PhotosLibrary
from rich.console import Console
from osxphotos._constants import APP_NAME
from osxphotos.compare_exif import PhotoCompare
from osxphotos.datetime_utils import datetime_naive_to_local, datetime_to_new_tz
from osxphotos.exif_datetime_updater import ExifDateTimeUpdater
from osxphotos.exiftool import get_exiftool_path
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from osxphotos.timeutils import update_datetime
from osxphotos.timezones import Timezone
from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
)
from .color_themes import get_theme
from .common import THEME_OPTION
from .darkmode import is_dark_mode
from .help import HELP_WIDTH, rich_text
from .param_types import DateOffset, DateTimeISO8601, TimeOffset, TimeString, UTCOffset
from .verbose import get_verbose_console, verbose_print
# format for pretty printing date/times
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z"
def update_photo_date_time(
photo: Photo,
date,
time,
date_delta,
time_delta,
verbose_print: Callable,
):
"""Update date, time in photo"""
photo_date = photo.date
new_photo_date = update_datetime(
photo_date, date=date, time=time, date_delta=date_delta, time_delta=time_delta
)
filename = photo.filename
uuid = photo.uuid
if new_photo_date != photo_date:
photo.date = new_photo_date
verbose_print(
f"Updated date/time for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]) from: [time]{photo_date}[/time] to [time]{new_photo_date}[/time]"
)
else:
verbose_print(
f"Skipped date/time update for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]): nothing to do"
)
def update_photo_time_for_new_timezone(
library_path: str,
photo: Photo,
new_timezone: Timezone,
verbose_print: Callable,
):
"""Update time in photo to keep it the same time but in a new timezone
For example, photo time is 12:00+0100 and new timezone is +0200,
so adjust photo time by 1 hour so it will now be 12:00+0200 instead of
13:00+0200 as it would be with no adjustment to the time"""
old_timezone = PhotoTimeZone(library_path=library_path).get_timezone(photo)[0]
# need to move time in opposite direction of timezone offset so that
# photo time is the same time but in the new timezone
delta = old_timezone - new_timezone.offset
photo_date = photo.date
new_photo_date = update_datetime(
dt=photo_date, time_delta=datetime.timedelta(seconds=delta)
)
filename = photo.filename
uuid = photo.uuid
if photo_date != new_photo_date:
photo.date = new_photo_date
verbose_print(
f"Adjusted date/time for photo [filename]{filename}[/filename] ([uuid]{uuid}[/uuid]) to match "
f"previous time [time]{photo_date}[time] but in new timezone [tz]{new_timezone}[/tz]."
)
else:
verbose_print(
f"Skipping date/time update for photo [filename]{filename}[/filename] ([uuid]{photo.uuid}[/uuid]), "
f"already matches new timezone [tz]{new_timezone}[/tz]"
)
class TimeWarpCommand(click.Command):
"""Custom click.Command that overrides get_help() to show additional help info for export"""
def get_help(self, ctx):
help_text = super().get_help(ctx)
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n\n")
formatter.write(
rich_text(
dedent(
"""
# Timewarp Overview
Timewarp operates on photos selected in Apple Photos. To use it, open Photos, select the photos for which you'd like to adjust the date/time/timezone, then run osxphotos timewarp from the command line:
`osxphotos timewarp --date 2021-09-10 --time-delta "-1 hour" --timezone -0700 --verbose`
This example sets the date for all selected photos to `2021-09-10`, subtracts 1 hour from the time of each photo, and sets the timezone of each photo to `GMT -07:00` (Pacific Daylight Time).
osxphotos timewarp has been well tested on macOS Catalina (10.15). It should work on macOS Big Sur (11.0) and macOS Monterey (12.0) but I have not been able to test this. It will not work on macOS Mojave (10.14) or earlier as the Photos database format is different.
**Caution**: This app directly modifies your Photos library database using undocumented features. It may corrupt, damage, or destroy your Photos library. Use at your own caution. I strongly recommend you make a backup of your Photos library before using this script (e.g. use Time Machine).
## Examples
**Add 1 day to the date of each photo**
`osxphotos timewarp --date-delta 1`
or
`osxphotos timewarp --date-delta "+1 day"`
**Set the date of each photo to 23 April 2020 and add 3 hours to the time**
`osxphotos timewarp --date 2020-04-23 --time-delta "+3 hours"`
or
`osxphotos timewarp --date 2020-04-23 --time-delta "+03:00:00"`
**Set the time of each photo to 14:30 and set the timezone to UTC +1:00 (Central European Time)**
`osxphotos timewarp --time 14:30 --timezone +01:00`
or
`osxphotos timewarp --time 14:30 --timezone +0100`
**Subtract 1 week from the date for each photo, add 3 hours to the time, set the timezone to UTC -07:00 (Pacific Daylight Time) and also use exiftool to update the EXIF metadata accordingly in the original file; use --verbose to print additional details**
`osxphotos timewarp --date-delta "-1 week" --time-delta "+3 hours" --timezone -0700 --push-exif --verbose`
For this to work, you'll need to install the third-party exiftool (https://exiftool.org/) utility. If you use homebrew (https://brew.sh/) you can do this with `brew install exiftool`.
**Set the timezone to UTC +03:00 for each photo but keep the time the same (that is, don't adjust time for the new timezone)**
`osxphotos timewarp --timezone 0300 --match-time`
*Note on timezones and times*: In Photos, when you change the timezone, Photos assumes the time itself was correct for the previous timezone and adjusts the time accordingly to the new timezone. E.g. if the photo's time is `13:00` and the timezone is `GMT -07:00` and you adjust the timezone one hour east to `GMT -06:00`, Photos will change the time of the photo to `14:00`. osxphotos timewarp follows this behavior. Using `--match-time` allows you to adjust the timezone but keep the same time without adjustment. For example, if your camera clock was correct but lacked timezone information and you took photos in one timezone but imported them to photos in another, Photos will add the timezone of the computer at time of import. You can use osxphotos timewarp to adjust the timezone but keep the time using `--match-time`.
**Compare the date/time/timezone of selected photos with the date/time/timezone in the photos' original EXIF metadata**
`osxphotos timewarp --compare-exif`
"""
),
width=formatter.width,
markdown=True,
)
)
help_text += formatter.getvalue()
return help_text
@click.command(cls=TimeWarpCommand, name="timewarp")
@click.option(
"--date",
"-d",
metavar="DATE",
type=DateTimeISO8601(),
help="Set date for selected photos. Format is 'YYYY-MM-DD'.",
)
@click.option(
"--date-delta",
"-D",
metavar="DELTA",
type=DateOffset(),
help="Adjust date for selected photos by DELTA. "
"Format is one of: '±D days', '±W weeks', '±D' where D is days",
)
@click.option(
"--time",
"-t",
metavar="TIME",
type=TimeString(),
help="Set time for selected photos. Format is one of 'HH:MM:SS', 'HH:MM:SS.fff', 'HH:MM'.",
)
@click.option(
"--time-delta",
"-T",
metavar="DELTA",
type=TimeOffset(),
help="Adjust time for selected photos by DELTA time. "
"Format is one of '±HH:MM:SS', '±H hours' (or hr), '±M minutes' (or min), '±S seconds' (or sec), '±S' (where S is seconds)",
)
@click.option(
"--timezone",
"-z",
metavar="TIMEZONE",
type=UTCOffset(),
help="Set timezone for selected photos as offset from UTC. "
"Format is one of '±HH:MM', '±H:MM', or '±HHMM'. "
"The actual time of the photo is not adjusted which means, somewhat counterintuitively, "
"that the time in the new timezone will be different. "
"For example, if photo has time of 12:00 and timezone of GMT+01:00 and new timezone is specified as "
"'--timezone +02:00' (one hour ahead of current GMT+01:00 timezone), the photo's new time will be 13:00 GMT+02:00, "
"which is equivalent to the old time of 12:00+01:00. "
"This is the same behavior exhibited by Photos when manually adjusting timezone in the Get Info window. "
"See also --match-time. ",
)
@click.option(
"--inspect",
"-i",
is_flag=True,
help="Print out the date/time/timezone for each selected photo without changing any information.",
)
@click.option(
"--compare-exif",
"-c",
is_flag=True,
help="Compare the EXIF date/time/timezone for each selected photo to the same data in Photos. "
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
"See also --add-to-album.",
)
@click.option(
"--push-exif",
"-p",
is_flag=True,
help="Push date/time and timezone for selected photos from Photos to the "
"EXIF metadata in the original file in the Photos library. "
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
"Using this option modifies the *original* file of the image in your Photos library. "
"--push-exif will be executed after any other updates are performed on the photo. "
"See also --pull-exif.",
)
@click.option(
"--pull-exif",
"-P",
is_flag=True,
help="Pull date/time and timezone for selected photos from EXIF metadata in the original file "
"into Photos and update the associated data in Photos to match the EXIF data. "
"--pull-exif will be executed before any other updates are performed on the photo. "
"It is possible for images to have missing EXIF data, for example the date/time could be set but there might be "
"no timezone set in the EXIF metadata. "
"Missing data will be handled thusly: if date/time/timezone are all present in the EXIF data, "
"the photo's date/time/timezone will be updated. If timezone is missing but date/time is present, "
"only the photo's date/time will be updated. If date/time is missing but the timezone is present, only the "
"photo's timezone will be updated unless --use-file-time is set in which case, "
"the photo's file modification date/time will be used in place of EXIF date/time. "
"If the date is present but the time is missing, the time will be set to 00:00:00. "
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
"See also --push-exif.",
)
# constraint=RequireAtLeast(1),
# @constraint(mutually_exclusive, ["date", "date_delta"])
# @constraint(mutually_exclusive, ["time", "time_delta"])
@click.option(
"--match-time",
"-m",
is_flag=True,
help="When used with --timezone, adjusts the photo time so that the timestamp in the new timezone matches "
"the timestamp in the old timezone. "
"For example, if photo has time of 12:00 and timezone of GMT+01:00 and new timezone is specified as "
"'--timezone +02:00' (one hour ahead of current GMT+01:00 timezone), the photo's new time will be 12:00 GMT+02:00. "
"That is, the timezone will have changed but the timestamp of the photo will match the previous timestamp. "
"Use --match-time when the camera's time was correct for the time the photo was taken but the "
"timezone was missing or wrong and you want to adjust the timezone while preserving the photo's time. "
"See also --timezone.",
)
@click.option(
"--use-file-time",
"-f",
is_flag=True,
help="When used with --pull-exif, the file modification date/time will be used if date/time "
"is missing from the EXIF data. ",
)
@click.option(
"--add-to-album",
"-a",
metavar="ALBUM",
help="When used with --compare-exif, adds any photos with date/time/timezone differences "
"between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.",
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Show verbose output.")
@click.option(
"--library",
"-L",
metavar="PHOTOS_LIBRARY_PATH",
type=click.Path(),
help=r"Path to Photos library (e.g. '~/Pictures/Photos\ Library.photoslibrary'). "
f"This is not likely needed as {APP_NAME} will usually be able to locate the path to the open Photos library. "
"Use --library only if you get an error that the Photos library cannot be located.",
)
@click.option(
"--exiftool-path",
"-e",
type=click.Path(exists=True),
help="Optional path to exiftool executable (will look in $PATH if not specified) for those options which require exiftool.",
)
@click.option(
"--output-file",
"-o",
type=click.File(mode="w", lazy=False),
help="Output file. If not specified, output is written to stdout.",
)
@click.option(
"--terminal-width",
"-w",
type=int,
help="Terminal width in characters.",
hidden=True,
)
# @constraint(mutually_exclusive, ["plain", "mono", "dark", "light"])
# @constraint(If("match_time", then=requires_one), ["timezone"])
# @constraint(If("add_to_album", then=requires_one), ["compare_exif"])
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
@THEME_OPTION
@click.option(
"--plain",
is_flag=True,
help="Plain text mode. Do not use rich output.",
)
def timewarp(
date,
date_delta,
time,
time_delta,
timezone,
inspect,
compare_exif,
push_exif,
pull_exif,
match_time,
use_file_time,
add_to_album,
exiftool_path,
verbose,
library,
theme,
plain,
output_file,
terminal_width,
timestamp,
):
"""Adjust date/time/timezone of photos in Apple Photos.
Changes will be applied to all photos currently selected in Photos.
timewarp cannot operate on photos selected in a Smart Album;
select photos in a regular album or in the 'All Photos' view.
See Timewarp Overview below for additional information.
"""
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose,
timestamp,
rich=True,
theme=color_theme,
highlight=False,
file=output_file,
)
# set console for rich_echo to be same as for verbose_
terminal_width = terminal_width or (1000 if output_file else None)
if output_file:
set_rich_console(Console(file=output_file, width=terminal_width))
elif terminal_width:
set_rich_console(
Console(file=sys.stdout, force_terminal=True, width=terminal_width)
)
else:
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
if any([compare_exif, push_exif, pull_exif]):
exiftool_path = exiftool_path or get_exiftool_path()
verbose_(f"exiftool path: [filename]{exiftool_path}[/filename]")
try:
photos = PhotosLibrary().selection
if not photos:
rich_echo_error("[warning]No photos selected[/]")
sys.exit(0)
except Exception as e:
# AppleScript error -1728 occurs if user attempts to get selected photos in a Smart Album
if "(-1728)" in str(e):
rich_echo_error(
"[error]Could not get selected photos. Ensure photos is open and photos are selected. "
"If you have selected photos and you see this message, it may be because the selected photos are in a Photos Smart Album. "
f"{APP_NAME} cannot access photos in a Smart Album. Select the photos in a regular album or in 'All Photos' view. "
"Another option is to create a new album using 'File | New Album With Selection' then select the photos in the new album.[/]",
)
else:
rich_echo_error(
f"[error]Could not get selected photos. Ensure Photos is open and photos to process are selected. {e}[/]",
)
sys.exit(1)
update_photo_date_time_ = partial(
update_photo_date_time,
date=date,
time=time,
date_delta=date_delta,
time_delta=time_delta,
verbose_print=verbose_,
)
update_photo_time_for_new_timezone_ = partial(
update_photo_time_for_new_timezone,
library_path=library,
verbose_print=verbose_,
)
if inspect:
tzinfo = PhotoTimeZone(library_path=library)
if photos:
rich_echo(
"[filename]filename[/filename], [uuid]uuid[/uuid], [time]photo time (local)[/time], [time]photo time[/time], [tz]timezone offset[/tz], [tz]timezone name[/tz]"
)
for photo in photos:
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
photo_date_local = datetime_naive_to_local(photo.date)
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
rich_echo(
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], [time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], [time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], [tz]{tz_str}[/tz], [tz]{tz_name}[/tz]"
)
sys.exit(0)
if compare_exif:
album = PhotosAlbumPhotoScript(add_to_album) if add_to_album else None
different_photos = 0
if photos:
photocomp = PhotoCompare(
library_path=library,
verbose=verbose_,
exiftool_path=exiftool_path,
)
if not album:
rich_echo(
"filename, uuid, photo time (Photos), photo time (EXIF), timezone offset (Photos), timezone offset (EXIF)"
)
for photo in photos:
diff_results = (
photocomp.compare_exif_with_markup(photo)
if not plain
else photocomp.compare_exif_no_markup(photo)
)
if not plain:
filename = (
f"[change]{photo.filename}[/change]"
if diff_results.diff
else f"[no_change]{photo.filename}[/no_change]"
)
else:
filename = photo.filename
uuid = f"[uuid]{photo.uuid}[/uuid]"
if album:
if diff_results.diff:
different_photos += 1
verbose_(
f"Photo {filename} ({uuid}) has different date/time/timezone, adding to album '{album.name}'"
)
album.add(photo)
else:
verbose_(f"Photo {filename} ({uuid}) has same date/time/timezone")
else:
rich_echo(
f"{filename}, {uuid}, "
f"{diff_results.photos_date} {diff_results.photos_time}, {diff_results.exif_date} {diff_results.exif_time}, "
f"{diff_results.photos_tz}, {diff_results.exif_tz}"
)
if album:
rich_echo(
f"Compared {len(photos)} photos, found {different_photos} "
f"that {pluralize(different_photos, 'is', 'are')} different and "
f"added {pluralize(different_photos, 'it', 'them')} to album '{album.name}'."
)
sys.exit(0)
if timezone:
tz_updater = PhotoTimeZoneUpdater(
timezone, verbose=verbose_, library_path=library
)
if any([push_exif, pull_exif]):
exif_updater = ExifDateTimeUpdater(
library_path=library,
verbose=verbose_,
exiftool_path=exiftool_path,
plain=plain,
)
rich_echo(f"Processing {len(photos)} {pluralize(len(photos), 'photo', 'photos')}")
# send progress bar output to /dev/null if verbose to hide the progress bar
fp = open(os.devnull, "w") if verbose else None
with click.progressbar(photos, file=fp) as bar:
for p in bar:
if pull_exif:
exif_updater.update_photos_from_exif(
p, use_file_modify_date=use_file_time
)
if any([date, time, date_delta, time_delta]):
update_photo_date_time_(p)
if match_time:
# need to adjust time before the timezone is updated
# or the old timezone will be overwritten in the database
update_photo_time_for_new_timezone_(photo=p, new_timezone=timezone)
if timezone:
tz_updater.update_photo(p)
if push_exif:
# this should be the last step in the if chain to ensure all Photos data is updated
# before exiftool is run
exif_warn, exif_error = exif_updater.update_exif_from_photos(p)
if exif_warn:
rich_echo_error(
f"[warning]Warning running exiftool: {exif_warn}[/]"
)
if exif_error:
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
if fp is not None:
fp.close()
rich_echo("Done.")
# if output_file:
# output_file.close()

View File

@ -61,6 +61,7 @@ def verbose_print(
rich: bool = False,
highlight: bool = False,
theme: t.Optional[Theme] = None,
file: t.Optional[t.IO] = None,
**kwargs: t.Any,
) -> t.Callable:
"""Create verbose function to print output
@ -71,6 +72,7 @@ def verbose_print(
rich: use rich.print instead of click.echo
highlight: if True, use automatic rich.print highlighting
theme: optional rich.theme.Theme object to use for formatting
file: optional file handle to write to instead of stdout
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
Returns:
@ -80,7 +82,10 @@ def verbose_print(
return noop
global _console
_console.console = Console(theme=theme, width=10_000)
if file:
_console.console = Console(theme=theme, file=file)
else:
_console.console = Console(theme=theme, width=10_000)
# closure to capture timestamp
def verbose_(*args):

167
osxphotos/compare_exif.py Normal file
View File

@ -0,0 +1,167 @@
""" PhotoCompare class to compare date/time/timezone in Photos to the exif data """
from collections import namedtuple
from typing import Callable, List, Optional, Tuple
from osxphotos import PhotosDB
from osxphotos.exiftool import ExifTool
from photoscript import Photo
from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz
from .exif_datetime_updater import get_exif_date_time_offset
from .phototz import PhotoTimeZone
from .utils import noop
ExifDiff = namedtuple(
"ExifDiff",
[
"diff",
"photos_date",
"photos_time",
"photos_tz",
"exif_date",
"exif_time",
"exif_tz",
],
)
def change(msg: str) -> str:
"""Add change tag to string"""
return f"[change]{msg}[/change]"
def no_change(msg: str) -> str:
"""Add no change tag to string"""
return f"[no_change]{msg}[/no_change]"
class PhotoCompare:
"""Class to compare date/time/timezone in Photos to the exif data"""
def __init__(
self,
library_path: Optional[str] = None,
verbose: Optional[Callable] = None,
exiftool_path: Optional[str] = None,
):
self.library_path = library_path
self.db = PhotosDB(self.library_path)
self.verbose = verbose or noop
self.exiftool_path = exiftool_path
self.phototz = PhotoTimeZone(self.library_path)
def compare_exif(self, photo: Photo) -> List[str]:
"""Compare date/time/timezone in Photos to the exif data
Args:
photo (Photo): Photo object to compare
Returns:
List of strings:
"""
photos_offset_seconds, photos_tz_str, _ = self.phototz.get_timezone(photo)
photos_date = datetime_naive_to_local(photo.date)
photos_date = datetime_to_new_tz(photos_date, photos_offset_seconds)
photos_date_str = photos_date.strftime("%Y-%m-%d %H:%M:%S")
photo_ = self.db.get_photo(photo.uuid)
if photo_path := photo_.path:
exif = ExifTool(filepath=photo_path, exiftool=self.exiftool_path)
exif_dict = exif.asdict()
exif_dt_offset = get_exif_date_time_offset(exif_dict)
exif_offset = exif_dt_offset.offset_str
exif_date = (
exif_dt_offset.datetime.strftime("%Y-%m-%d %H:%M:%S")
if exif_dt_offset.datetime
else ""
)
else:
exif_date = ""
exif_offset = ""
return [photos_date_str, photos_tz_str, exif_date, exif_offset]
def compare_exif_with_markup(self, photo: Photo) -> ExifDiff:
"""Compare date/time/timezone in Photos to the exif data and return an ExifDiff named tuple;
adds rich markup to strings to show differences
Args:
photo (Photo): Photo object to compare
"""
photos_date, photos_tz, exif_date, exif_tz = self.compare_exif(photo)
diff = False
photos_date, photos_time = photos_date.split(" ", 1)
try:
exif_date, exif_time = exif_date.split(" ", 1)
except ValueError:
exif_date = exif_date
exif_time = ""
if photos_date != exif_date:
photos_date = change(photos_date)
exif_date = change(exif_date)
diff = True
else:
photos_date = no_change(photos_date)
exif_date = no_change(exif_date)
if photos_time != exif_time:
photos_time = change(photos_time)
exif_time = change(exif_time)
diff = True
else:
photos_time = no_change(photos_time)
exif_time = no_change(exif_time)
if photos_tz != exif_tz:
photos_tz = change(photos_tz)
exif_tz = change(exif_tz)
diff = True
else:
photos_tz = no_change(photos_tz)
exif_tz = no_change(exif_tz)
return ExifDiff(
diff,
photos_date,
photos_time,
photos_tz,
exif_date,
exif_time,
exif_tz,
)
def compare_exif_no_markup(self, photo: Photo) -> ExifDiff:
"""Compare date/time/timezone in Photos to the exif data and return an ExifDiff named tuple;
Args:
photo (Photo): Photo object to compare
"""
photos_date, photos_tz, exif_date, exif_tz = self.compare_exif(photo)
diff = False
photos_date, photos_time = photos_date.split(" ", 1)
try:
exif_date, exif_time = exif_date.split(" ", 1)
except ValueError:
exif_date = exif_date
exif_time = ""
if photos_date != exif_date:
diff = True
if photos_time != exif_time:
diff = True
if photos_tz != exif_tz:
diff = True
return ExifDiff(
diff,
photos_date,
photos_time,
photos_tz,
exif_date,
exif_time,
exif_tz,
)

View File

@ -1,19 +1,28 @@
""" datetime.datetime helper functions for converting to/from UTC """
""" datetime.datetime helper functions for converting to/from UTC and other datetime manipulations"""
# source: https://github.com/RhetTbull/datetime-utils
__version__ = "2022.04.30"
import datetime
# TODO: probably shouldn't use replace here, see this:
# https://stackoverflow.com/questions/13994594/how-to-add-timezone-into-a-naive-datetime-instance-in-python/13994611#13994611
__all__ = [
"get_local_tz",
"datetime_has_tz",
"datetime_tz_to_utc",
"datetime_remove_tz",
"datetime_naive_to_utc",
"datetime_naive_to_local",
"datetime_naive_to_utc",
"datetime_remove_tz",
"datetime_to_new_tz",
"datetime_tz_to_utc",
"datetime_utc_to_local",
"get_local_tz",
"utc_offset_seconds",
]
def get_local_tz(dt):
# TODO: look at https://github.com/regebro/tzlocal for more robust implementation
def get_local_tz(dt: datetime.datetime) -> datetime.tzinfo:
"""Return local timezone as datetime.timezone tzinfo for dt
Args:
@ -31,7 +40,7 @@ def get_local_tz(dt):
raise ValueError("dt must be naive datetime.datetime object")
def datetime_has_tz(dt):
def datetime_has_tz(dt: datetime.datetime) -> bool:
"""Return True if datetime dt has tzinfo else False
Args:
@ -50,7 +59,7 @@ def datetime_has_tz(dt):
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def datetime_tz_to_utc(dt):
def datetime_tz_to_utc(dt: datetime.datetime) -> datetime.datetime:
"""Convert datetime.datetime object with timezone to UTC timezone
Args:
@ -70,10 +79,10 @@ def datetime_tz_to_utc(dt):
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
return dt.replace(tzinfo=dt.tzinfo).astimezone(tz=datetime.timezone.utc)
else:
raise ValueError(f"dt does not have timezone info")
raise ValueError("dt does not have timezone info")
def datetime_remove_tz(dt):
def datetime_remove_tz(dt: datetime.datetime) -> datetime.datetime:
"""Remove timezone from a datetime.datetime object
Args:
@ -92,7 +101,7 @@ def datetime_remove_tz(dt):
return dt.replace(tzinfo=None)
def datetime_naive_to_utc(dt):
def datetime_naive_to_utc(dt: datetime.datetime) -> datetime.datetime:
"""Convert naive (timezone unaware) datetime.datetime
to aware timezone in UTC timezone
@ -120,7 +129,7 @@ def datetime_naive_to_utc(dt):
return dt.replace(tzinfo=datetime.timezone.utc)
def datetime_naive_to_local(dt):
def datetime_naive_to_local(dt: datetime.datetime) -> datetime.datetime:
"""Convert naive (timezone unaware) datetime.datetime
to aware timezone in local timezone
@ -142,13 +151,13 @@ def datetime_naive_to_local(dt):
# has timezone info
raise ValueError(
"dt must be naive/timezone unaware: "
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tizinfo.utcoffset(dt)}"
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tzinfo.utcoffset(dt)}"
)
return dt.replace(tzinfo=get_local_tz(dt))
def datetime_utc_to_local(dt):
def datetime_utc_to_local(dt: datetime.datetime) -> datetime.datetime:
"""Convert datetime.datetime object in UTC timezone to local timezone
Args:
@ -169,3 +178,33 @@ def datetime_utc_to_local(dt):
raise ValueError(f"{dt} must be in UTC timezone: timezone = {dt.tzinfo}")
return dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None)
def datetime_to_new_tz(dt: datetime.datetime, offset) -> datetime.datetime:
"""Convert datetime.datetime object from current timezone to new timezone with offset of seconds from UTC"""
if not datetime_has_tz(dt):
raise ValueError("dt must be timezone aware")
time_delta = datetime.timedelta(seconds=offset)
tz = datetime.timezone(time_delta)
return dt.astimezone(tz=tz)
def utc_offset_seconds(dt: datetime.datetime) -> int:
"""Return offset in seconds from UTC for timezone aware datetime.datetime object
Args:
dt: datetime.datetime object
Returns:
offset in seconds from UTC
Raises:
ValueError if dt does not have timezone information
"""
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
return dt.tzinfo.utcoffset(dt).total_seconds()
else:
raise ValueError("dt does not have timezone info")

Binary file not shown.

View File

@ -0,0 +1,336 @@
"""Use exiftool to update exif data in photos """
import datetime
import re
from collections import namedtuple
from typing import Callable, Dict, Optional, Tuple
from photoscript import Photo
from .datetime_utils import (
datetime_has_tz,
datetime_naive_to_local,
datetime_remove_tz,
datetime_to_new_tz,
datetime_tz_to_utc,
datetime_utc_to_local,
)
from .exiftool import ExifTool
from .photosdb import PhotosDB
from .phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from .timezones import Timezone, format_offset_time
from .utils import noop
__all__ = ["ExifDateTime", "ExifDateTimeUpdater"]
# date/time/timezone extracted from regex as a timezone aware datetime.datetime object
# default_time is True if the time is not specified in the exif otherwise False (and if True, set to 00:00:00)
# default_offset is True if timezone offset is not specified in the exif otherwise False (and if True, set to +00:00)
# used_file_modify_date is True if the date/time is not specified in the exif and the FileModifyDate is used instead
ExifDateTime = namedtuple(
"ExifDateTime",
[
"datetime",
"offset_seconds",
"offset_str",
"default_time",
"used_file_modify_date",
],
)
def exif_offset_to_seconds(offset: str) -> int:
"""Convert timezone offset from UTC in exiftool format (+/-hh:mm) to seconds"""
sign = 1 if offset[0] == "+" else -1
hours, minutes = offset[1:].split(":")
return sign * (int(hours) * 3600 + int(minutes) * 60)
class ExifDateTimeUpdater:
"""Update exif data in photos"""
def __init__(
self,
library_path: Optional[str] = None,
verbose: Optional[Callable] = None,
exiftool_path: Optional[str] = None,
plain=False,
):
self.library_path = library_path
self.db = PhotosDB(self.library_path)
self.verbose = verbose or noop
self.exiftool_path = exiftool_path
self.tzinfo = PhotoTimeZone(library_path=self.library_path)
self.plain = plain
def filename_color(self, filename: str) -> str:
"""Colorize filename for display in verbose output"""
return filename if self.plain else f"[filename]{filename}[/filename]"
def uuid_color(self, uuid: str) -> str:
"""Colorize uuid for display in verbose output"""
return uuid if self.plain else f"[uuid]{uuid}[/uuid]"
def update_exif_from_photos(self, photo: Photo) -> Tuple[str, str]:
"""Update EXIF data in photo to match the date/time/timezone in Photos library
Args:
photo: photoscript.Photo object to act on
"""
# photo is the photoscript.Photo object passed in
# _photo is the osxphotos.PhotoInfo object for the same photo
# Need _photo to get the photo's path
_photo = self.db.get_photo(photo.uuid)
if not _photo:
raise ValueError(f"Photo {photo.uuid} not found")
if not _photo.path:
self.verbose(
"Skipping EXIF update for missing photo "
f"[filename]{_photo.original_filename}[/filename] ([uuid]{_photo.uuid}[/uuid])"
)
return "", ""
self.verbose(
"Updating EXIF data for "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid])"
)
photo_date = datetime_naive_to_local(photo.date)
timezone_offset = self.tzinfo.get_timezone(photo)[0]
photo_date = datetime_to_new_tz(photo_date, timezone_offset)
# exiftool expects format to "2015:01:18 12:00:00"
datetimeoriginal = photo_date.strftime("%Y:%m:%d %H:%M:%S")
# exiftool expects format of "-04:00"
offset = format_offset_time(timezone_offset)
# process date/time and timezone offset
# Photos exports the following fields and sets modify date to creation date
# [EXIF] Date/Time Original : 2020:10:30 00:00:00
# [EXIF] Create Date : 2020:10:30 00:00:00
# [IPTC] Digital Creation Date : 2020:10:30
# [IPTC] Date Created : 2020:10:30
#
# for videos:
# [QuickTime] CreateDate : 2020:12:11 06:10:10
# [Keys] CreationDate : 2020:12:10 22:10:10-08:00
exif = {}
if _photo.isphoto:
exif["EXIF:DateTimeOriginal"] = datetimeoriginal
exif["EXIF:CreateDate"] = datetimeoriginal
dateoriginal = photo_date.strftime("%Y:%m:%d")
exif["IPTC:DateCreated"] = dateoriginal
timeoriginal = photo_date.strftime(f"%H:%M:%S{offset}")
exif["IPTC:TimeCreated"] = timeoriginal
exif["EXIF:OffsetTimeOriginal"] = offset
elif _photo.ismovie:
# QuickTime spec specifies times in UTC
# QuickTime:CreateDate and ModifyDate are in UTC w/ no timezone
# QuickTime:CreationDate must include time offset or Photos shows invalid values
# reference: https://exiftool.org/TagNames/QuickTime.html#Keys
# https://exiftool.org/forum/index.php?topic=11927.msg64369#msg64369
creationdate = f"{datetimeoriginal}{offset}"
exif["QuickTime:CreationDate"] = creationdate
# need to convert to UTC then back to formatted string
tzdate = datetime.datetime.strptime(creationdate, "%Y:%m:%d %H:%M:%S%z")
utcdate = datetime_tz_to_utc(tzdate)
createdate = utcdate.strftime("%Y:%m:%d %H:%M:%S")
exif["QuickTime:CreateDate"] = createdate
self.verbose(
f"Writing EXIF data with exiftool to {self.filename_color(_photo.path)}"
)
with ExifTool(filepath=_photo.path, exiftool=self.exiftool_path) as exiftool:
for tag, val in exif.items():
if type(val) == list:
for v in val:
exiftool.setvalue(tag, v)
else:
exiftool.setvalue(tag, val)
return exiftool.warning, exiftool.error
def update_photos_from_exif(
self, photo: Photo, use_file_modify_date: bool = False
) -> None:
"""Update date/time/timezone in Photos library to match the data in EXIF
Args:
photo: photoscript.Photo object to act on
use_file_modify_date: if True, use the file modify date if there's no date/time in the exif data
"""
# photo is the photoscript.Photo object passed in
# _photo is the osxphotos.PhotoInfo object for the same photo
# Need _photo to get the photo's path
_photo = self.db.get_photo(photo.uuid)
if not _photo:
raise ValueError(f"Photo {photo.uuid} not found")
if not _photo.path:
self.verbose(
"Skipping EXIF update for missing photo "
f"[filename]{_photo.original_filename}[/filename] ([uuid]{_photo.uuid}[/uuid])"
)
return None
self.verbose(
"Updating Photos from EXIF data for "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid])"
)
dtinfo = self.get_date_time_offset_from_exif(
_photo.path, use_file_modify_date=use_file_modify_date
)
if dtinfo.used_file_modify_date:
self.verbose(
"EXIF date/time missing, using file modify date/time for "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid])"
)
if not dtinfo.datetime and not dtinfo.offset_seconds:
self.verbose(
"Skipping update for missing EXIF data in photo "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid])"
)
return None
if dtinfo.offset_seconds:
# update timezone then update date/time
timezone = Timezone(dtinfo.offset_seconds)
tzupdater = PhotoTimeZoneUpdater(
library_path=self.library_path, timezone=timezone
)
tzupdater.update_photo(photo)
self.verbose(
"Updated timezone offset for photo "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]): [tz]{timezone}[/tz]"
)
if dtinfo.datetime:
if datetime_has_tz(dtinfo.datetime):
# convert datetime to naive local time for setting in photos
local_datetime = datetime_remove_tz(
datetime_utc_to_local(datetime_tz_to_utc(dtinfo.datetime))
)
else:
local_datetime = dtinfo.datetime
# update date/time
photo.date = local_datetime
self.verbose(
"Updated date/time for photo "
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]): [time]{local_datetime}[/time]"
)
return None
def get_date_time_offset_from_exif(
self, photo_path: str, use_file_modify_date: bool = False
) -> ExifDateTime:
"""Get date/time/timezone from EXIF data for a photo
Args:
photo_path: path to photo to get EXIF data from
use_file_modify_date: if True, use the file modify date if there's no date/time in the exif data
Returns:
ExifDateTime named tuple
"""
exiftool = ExifTool(filepath=photo_path, exiftool=self.exiftool_path)
exif = exiftool.asdict()
return get_exif_date_time_offset(
exif, use_file_modify_date=use_file_modify_date
)
def get_exif_date_time_offset(
exif: Dict, use_file_modify_date: bool = False
) -> ExifDateTime:
"""Get datetime/offset from an exif dict as returned by osxphotos.exiftool.ExifTool.asdict()
Args:
exif: dict of exif data
use_file_modify_date: if True, use the file modify date if there's no date/time in the exif data
"""
# set to True if no time is found
default_time = False
# set to True if no date/time in EXIF and the FileModifyDate is used
used_file_modify_date = False
# search these fields in this order for date/time/timezone
time_fields = [
"EXIF:DateTimeOriginal",
"EXIF:CreateDate",
"QuickTime:CreationDate",
"QuickTime:CreateDate",
"IPTC:DateCreated",
"XMP-exif:DateTimeOriginal",
"XMP-xmp:CreateDate",
]
if use_file_modify_date:
time_fields.append("File:FileModifyDate")
for dt_str in time_fields:
dt = exif.get(dt_str)
if dt and dt_str == "IPTC:DateCreated":
# also need time
time_ = exif.get("IPTC:TimeCreated")
if not time_:
time_ = "00:00:00"
default_time = True
dt = f"{dt} {time_}"
if dt:
used_file_modify_date = dt_str == "File:FileModifyDate"
break
else:
# no date/time found
dt = None
# try to get offset from EXIF:OffsetTimeOriginal
offset = exif.get("EXIF:OffsetTimeOriginal")
if dt and not offset:
# see if offset set in the dt string
matched = re.match(r"\d{4}:\d{2}:\d{2}\s\d{2}:\d{2}:\d{2}([+-]\d{2}:\d{2})", dt)
offset = matched.group(1) if matched else None
if dt:
# make sure we have time
matched = re.match(r"\d{4}:\d{2}:\d{2}\s(\d{2}:\d{2}:\d{2})", dt)
if not matched:
if matched := re.match(r"^(\d{4}:\d{2}:\d{2})", dt):
# set time to 00:00:00
dt = f"{matched.group(1)} 00:00:00"
default_time = True
offset_seconds = exif_offset_to_seconds(offset) if offset else None
if dt:
if offset:
# drop offset from dt string and add it back on in datetime %z format
dt = re.sub(r"[+-]\d{2}:\d{2}$", "", dt)
offset = offset.replace(":", "")
dt = f"{dt}{offset}"
dt_format = "%Y:%m:%d %H:%M:%S%z"
else:
dt_format = "%Y:%m:%d %H:%M:%S"
# convert to datetime
# some files can have bad date/time data, (e.g. #24, Date/Time Original = 0000:00:00 00:00:00)
try:
dt = datetime.datetime.strptime(dt, dt_format)
except ValueError:
dt = None
# format offset in form +/-hhmm
offset_str = offset.replace(":", "") if offset else ""
return ExifDateTime(
dt, offset_seconds, offset_str, default_time, used_file_modify_date
)

View File

@ -4,14 +4,17 @@ from typing import List, Optional
import photoscript
from more_itertools import chunked
from photoscript import Photo, PhotosLibrary
from .photoinfo import PhotoInfo
from .utils import noop
from .utils import noop, pluralize
__all__ = ["PhotosAlbum"]
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]
class PhotosAlbum:
"""Add osxphotos.photoinfo.PhotoInfo objects to album"""
def __init__(self, name: str, verbose: Optional[callable] = None):
self.name = name
self.verbose = verbose or noop
@ -39,9 +42,40 @@ class PhotosAlbum:
self.verbose(f"Error creating Photo object for photo {p.uuid}: {e}")
for photolist in chunked(photos, 10):
self.album.add(photolist)
photo_len = len(photos)
photo_word = "photos" if photo_len > 1 else "photo"
self.verbose(f"Added {photo_len} {photo_word} to album {self.name}")
photo_len = len(photo_list)
self.verbose(
f"Added {photo_len} {pluralize(photo_len, 'photo', 'photos')} to album {self.name}"
)
def photos(self):
return self.album.photos()
class PhotosAlbumPhotoScript:
"""Add photoscript.Photo objects to album"""
def __init__(self, name: str, verbose: Optional[callable] = None):
self.name = name
self.verbose = verbose or noop
self.library = PhotosLibrary()
album = self.library.album(name)
if album is None:
self.verbose(f"Creating Photos album '{self.name}'")
album = self.library.create_album(name)
self.album = album
def add(self, photo: Photo):
self.album.add([photo])
self.verbose(f"Added {photo.filename} ({photo.uuid}) to album {self.name}")
def add_list(self, photo_list: List[Photo]):
for photolist in chunked(photo_list, 10):
self.album.add(photolist)
photo_len = len(photo_list)
self.verbose(
f"Added {photo_len} {pluralize(photo_len, 'photo', 'photos')} to album {self.name}"
)
def photos(self):
return self.album.photos()

156
osxphotos/phototz.py Normal file
View File

@ -0,0 +1,156 @@
""" Update the timezone of a photo in Apple Photos' library """
# WARNING: This is a hack. It might destroy your Photos library.
# Ensure you have a backup before using!
# You have been warned.
import pathlib
import sqlite3
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 .photosdb.photosdb_utils import get_photos_library_version
from .timezones import Timezone
from .utils import get_last_library_path, get_system_library_path, noop
def tz_to_str(tz_seconds: int) -> str:
"""convert timezone offset in seconds to string in form +00:00 (as offset from GMT)"""
sign = "+" if tz_seconds >= 0 else "-"
tz_seconds = abs(tz_seconds)
# get min and seconds first
mm, _ = divmod(tz_seconds, 60)
# Get hours
hh, mm = divmod(mm, 60)
return f"{sign}{hh:02}{mm:02}"
class PhotoTimeZone:
"""Get timezone info for photos"""
def __init__(
self,
library_path: Optional[str] = None,
):
# get_last_library_path() returns the path to the last Photos library
# opened but sometimes (rarely) fails on some systems
try:
db_path = (
library_path or get_last_library_path() or get_system_library_path()
)
except Exception:
db_path = None
if not db_path:
raise FileNotFoundError("Could not find Photos database path")
photos_version = get_photos_library_version(db_path)
db_path = str(pathlib.Path(db_path) / "database/Photos.sqlite")
self.db_path = db_path
self.ASSET_TABLE = _DB_TABLE_NAMES[photos_version]["ASSET"]
def get_timezone(self, photo: Photo) -> Tuple[int, str, str]:
"""Return (timezone_seconds, timezone_str, timezone_name) of photo"""
uuid = photo.uuid
sql = f""" SELECT
ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET,
ZADDITIONALASSETATTRIBUTES.ZTIMEZONENAME
FROM ZADDITIONALASSETATTRIBUTES
JOIN {self.ASSET_TABLE}
ON ZADDITIONALASSETATTRIBUTES.ZASSET = {self.ASSET_TABLE}.Z_PK
WHERE {self.ASSET_TABLE}.ZUUID = '{uuid}'
"""
with sqlite3.connect(self.db_path) as conn:
c = conn.cursor()
c.execute(sql)
results = c.fetchone()
tz, tzname = (results[0], results[1])
tz_str = tz_to_str(tz)
return tz, tz_str, tzname
class PhotoTimeZoneUpdater:
"""Update timezones for Photos objects"""
def __init__(
self,
timezone: Timezone,
verbose: Optional[Callable] = None,
library_path: Optional[str] = None,
):
self.timezone = timezone
self.tz_offset = timezone.offset
self.tz_name = timezone.name
self.verbose = verbose or noop
# get_last_library_path() returns the path to the last Photos library
# opened but sometimes (rarely) fails on some systems
try:
db_path = (
library_path or get_last_library_path() or get_system_library_path()
)
except Exception:
db_path = None
if not db_path:
raise FileNotFoundError("Could not find Photos database path")
photos_version = get_photos_library_version(db_path)
db_path = str(pathlib.Path(db_path) / "database/Photos.sqlite")
self.db_path = db_path
self.ASSET_TABLE = _DB_TABLE_NAMES[photos_version]["ASSET"]
def update_photo(self, photo: Photo):
"""Update the timezone of a photo in the database
Args:
photo: Photo object to update
"""
try:
self._update_photo(photo)
except Exception as e:
self.verbose(f"Error updating {photo.uuid}: {e}")
@retry(
wait=wait_exponential(multiplier=1, min=0.100, max=5),
stop=stop_after_attempt(10),
)
def _update_photo(self, photo: Photo):
try:
uuid = photo.uuid
sql = f""" SELECT
ZADDITIONALASSETATTRIBUTES.Z_PK,
ZADDITIONALASSETATTRIBUTES.Z_OPT,
ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET,
ZADDITIONALASSETATTRIBUTES.ZTIMEZONENAME
FROM ZADDITIONALASSETATTRIBUTES
JOIN {self.ASSET_TABLE}
ON ZADDITIONALASSETATTRIBUTES.ZASSET = {self.ASSET_TABLE}.Z_PK
WHERE {self.ASSET_TABLE}.ZUUID = '{uuid}'
"""
with sqlite3.connect(self.db_path) as conn:
c = conn.cursor()
c.execute(sql)
results = c.fetchone()
z_opt = results[1] + 1
z_pk = results[0]
tz_offset = results[2]
tz_name = results[3]
sql_update = f""" UPDATE ZADDITIONALASSETATTRIBUTES
SET Z_OPT={z_opt},
ZTIMEZONEOFFSET={self.tz_offset},
ZTIMEZONENAME='{self.tz_name}'
WHERE Z_PK={z_pk};
"""
with sqlite3.connect(self.db_path) as conn:
c = conn.cursor()
c.execute(sql_update)
conn.commit()
self.verbose(
f"Updated timezone for photo [filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]) "
+ f"from [tz]{[tz_name]}[/tz], offset=[tz]{tz_offset}[/tz] "
+ f"to [tz]{self.tz_name}[/tz], offset=[tz]{self.tz_offset}[/tz]"
)
except Exception as e:
raise e

84
osxphotos/timeutils.py Normal file
View File

@ -0,0 +1,84 @@
"""Utilities for working with datetimes"""
import datetime
from typing import Optional
import re
def utc_offset_string_to_seconds(utc_offset: str) -> int:
"""match a UTC offset in format ±[hh]:[mm], ±[h]:[mm], or ±[hh][mm] and return number of seconds offset"""
patterns = ["^([+-]?)(\d{1,2}):(\d{2})$", "^([+-]?)(\d{2})(\d{2})$"]
for pattern in patterns:
match = re.match(pattern, utc_offset)
if not match:
continue
sign = match[1]
hours = int(match[2])
minutes = int(match[3])
if sign == "-":
hours = -hours
minutes = -minutes
return (hours * 60 + minutes) * 60
raise ValueError(f"Invalid UTC offset format: {utc_offset}.")
def update_datetime(
dt: datetime.datetime,
date: Optional[datetime.date] = None,
time: Optional[datetime.time] = None,
date_delta: Optional[datetime.timedelta] = None,
time_delta: Optional[datetime.timedelta] = None,
) -> datetime.datetime:
"""
Update the date and time of a datetime object.
Args:
dt: datetime object
date: new date
time: new time
date_delta: a timedelta to apply
time_delta: a timedelta to apply
"""
if date is not None:
dt = dt.replace(year=date.year, month=date.month, day=date.day)
if time is not None:
dt = dt.replace(
hour=time.hour,
minute=time.minute,
second=time.second,
microsecond=time.microsecond,
)
if date_delta is not None:
dt = dt + date_delta
if time_delta is not None:
dt = dt + time_delta
return dt
def time_string_to_datetime(time: str) -> datetime.datetime:
"""Convert time string to datetime.datetime"""
""" valid time formats:
- HH:MM:SS,
- HH:MM:SS.fff,
- HH:MM,
"""
time_formats = [
"%H:%M:%S",
"%H:%M:%S.%f",
"%H:%M",
]
for dt_format in time_formats:
try:
parsed_dt = datetime.datetime.strptime(time, dt_format)
except ValueError as e:
pass
else:
return parsed_dt
raise ValueError(
f"Could not parse time format: {time} does not match {time_formats}"
)

56
osxphotos/timezones.py Normal file
View File

@ -0,0 +1,56 @@
"""Get list of valid timezones on macOS"""
from typing import Union
import Foundation
import objc
def known_timezone_names():
"""Get list of valid timezones on macOS"""
return Foundation.NSTimeZone.knownTimeZoneNames()
def format_offset_time(offset: int) -> str:
"""Format offset time to exiftool format: -04:00"""
sign = "-" if offset < 0 else "+"
hours, remainder = divmod(abs(offset), 3600)
minutes, _ = divmod(remainder, 60)
return f"{sign}{hours:02d}:{minutes:02d}"
class Timezone:
"""Create Timezone object from either name (str) or offset from GMT (int)"""
def __init__(self, tz: Union[str, int]):
with objc.autorelease_pool():
if isinstance(tz, str):
self.timezone = Foundation.NSTimeZone.timeZoneWithName_(tz)
self._name = tz
elif isinstance(tz, int):
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
self._name = self.timezone.name()
else:
raise TypeError("Timezone must be a string or an int")
@property
def name(self) -> str:
return self._name
@property
def offset(self) -> int:
return self.timezone.secondsFromGMT()
@property
def offset_str(self) -> str:
return format_offset_time(self.offset)
@property
def abbreviation(self) -> str:
return self.timezone.abbreviation()
def __str__(self):
return self.name
def __repr__(self):
return self.name

View File

@ -38,6 +38,7 @@ __all__ = [
"noop",
"normalize_fs_path",
"normalize_unicode",
"pluralize",
]
@ -511,3 +512,8 @@ def get_latest_version() -> Tuple[Optional[str], str]:
return data["info"]["version"], ""
except Exception as e:
return None, e
def pluralize(count, singular, plural):
"""Return singular or plural based on count"""
return singular if count == 1 else plural

View File

@ -19,10 +19,11 @@ pyobjc-framework-Metal>=7.3,<9.0
pyobjc-framework-Photos>=7.3,<9.0
pyobjc-framework-Quartz>=7.3,<9.0
pyobjc-framework-Vision>=7.3,<9.0
pytimeparse==1.1.8
PyYAML>=5.4.1,<6.0.0
requests>=2.27.1,<3.0.0
rich>=11.2.0,<13.0.0
rich_theme_manager>=0.9.0
rich_theme_manager>=0.11.0
tenacity>=8.0.1,<9.0.0
textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0

View File

@ -95,9 +95,10 @@ setup(
"pyobjc-framework-Photos>=7.3,<9.0",
"pyobjc-framework-Quartz>=7.3,<9.0",
"pyobjc-framework-Vision>=7.3,<9.0",
"pytimeparse==1.1.8",
"requests>=2.27.1,<3.0.0",
"rich>=11.2.0,<13.0.0",
"rich_theme_manager>=0.9.0",
"rich_theme_manager>=0.11.0",
"tenacity>=8.0.1,<9.0.0",
"textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0",

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LibrarySchemaVersion</key>
<integer>5001</integer>
<key>MetaSchemaVersion</key>
<integer>3</integer>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>hostname</key>
<string>Rhets-MacBook-Pro.local</string>
<key>hostuuid</key>
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
<key>pid</key>
<integer>1961</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>
<integer>501</integer>
</dict>
</plist>

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BlacklistedMeaningsByMeaning</key>
<dict/>
<key>MePersonUUID</key>
<string>39488755-78C0-40B2-B378-EDA280E1823C</string>
<key>SceneWhitelist</key>
<array>
<string>Graduation</string>
<string>Aquarium</string>
<string>Food</string>
<string>Ice Skating</string>
<string>Mountain</string>
<string>Cliff</string>
<string>Basketball</string>
<string>Tennis</string>
<string>Jewelry</string>
<string>Cheese</string>
<string>Softball</string>
<string>Football</string>
<string>Circus</string>
<string>Jet Ski</string>
<string>Playground</string>
<string>Carousel</string>
<string>Paint Ball</string>
<string>Windsurfing</string>
<string>Sailboat</string>
<string>Sunbathing</string>
<string>Dam</string>
<string>Fireplace</string>
<string>Flower</string>
<string>Scuba</string>
<string>Hiking</string>
<string>Cetacean</string>
<string>Pier</string>
<string>Bowling</string>
<string>Snowboarding</string>
<string>Zoo</string>
<string>Snowmobile</string>
<string>Theater</string>
<string>Boat</string>
<string>Casino</string>
<string>Car</string>
<string>Diving</string>
<string>Cycling</string>
<string>Musical Instrument</string>
<string>Board Game</string>
<string>Castle</string>
<string>Sunset Sunrise</string>
<string>Martial Arts</string>
<string>Motocross</string>
<string>Submarine</string>
<string>Cat</string>
<string>Snow</string>
<string>Kiteboarding</string>
<string>Squash</string>
<string>Geyser</string>
<string>Music</string>
<string>Archery</string>
<string>Desert</string>
<string>Blackjack</string>
<string>Fireworks</string>
<string>Sportscar</string>
<string>Feline</string>
<string>Soccer</string>
<string>Museum</string>
<string>Baby</string>
<string>Fencing</string>
<string>Railroad</string>
<string>Nascar</string>
<string>Sky Surfing</string>
<string>Bird</string>
<string>Games</string>
<string>Baseball</string>
<string>Dressage</string>
<string>Snorkeling</string>
<string>Pyramid</string>
<string>Kite</string>
<string>Rowboat</string>
<string>Golf</string>
<string>Watersports</string>
<string>Lightning</string>
<string>Canyon</string>
<string>Auditorium</string>
<string>Night Sky</string>
<string>Karaoke</string>
<string>Skiing</string>
<string>Parade</string>
<string>Forest</string>
<string>Hot Air Balloon</string>
<string>Dragon Parade</string>
<string>Easter Egg</string>
<string>Monument</string>
<string>Jungle</string>
<string>Thanksgiving</string>
<string>Jockey Horse</string>
<string>Stadium</string>
<string>Airplane</string>
<string>Ballet</string>
<string>Yoga</string>
<string>Coral Reef</string>
<string>Skating</string>
<string>Wrestling</string>
<string>Bicycle</string>
<string>Tattoo</string>
<string>Amusement Park</string>
<string>Canoe</string>
<string>Cheerleading</string>
<string>Ping Pong</string>
<string>Fishing</string>
<string>Magic</string>
<string>Reptile</string>
<string>Winter Sport</string>
<string>Waterfall</string>
<string>Train</string>
<string>Bonsai</string>
<string>Surfing</string>
<string>Dog</string>
<string>Cake</string>
<string>Sledding</string>
<string>Sandcastle</string>
<string>Glacier</string>
<string>Lighthouse</string>
<string>Equestrian</string>
<string>Rafting</string>
<string>Shore</string>
<string>Hockey</string>
<string>Santa Claus</string>
<string>Formula One Car</string>
<string>Sport</string>
<string>Vehicle</string>
<string>Boxing</string>
<string>Rollerskating</string>
<string>Underwater</string>
<string>Orchestra</string>
<string>Carnival</string>
<string>Rocket</string>
<string>Skateboarding</string>
<string>Helicopter</string>
<string>Performance</string>
<string>Oktoberfest</string>
<string>Water Polo</string>
<string>Skate Park</string>
<string>Animal</string>
<string>Nightclub</string>
<string>String Instrument</string>
<string>Dinosaur</string>
<string>Gymnastics</string>
<string>Cricket</string>
<string>Volcano</string>
<string>Lake</string>
<string>Aurora</string>
<string>Dancing</string>
<string>Concert</string>
<string>Rock Climbing</string>
<string>Hang Glider</string>
<string>Rodeo</string>
<string>Fish</string>
<string>Art</string>
<string>Motorcycle</string>
<string>Volleyball</string>
<string>Wake Boarding</string>
<string>Badminton</string>
<string>Motor Sport</string>
<string>Sumo</string>
<string>Parasailing</string>
<string>Skydiving</string>
<string>Kickboxing</string>
<string>Pinata</string>
<string>Foosball</string>
<string>Go Kart</string>
<string>Poker</string>
<string>Kayak</string>
<string>Swimming</string>
<string>Atv</string>
<string>Beach</string>
<string>Dartboard</string>
<string>Athletics</string>
<string>Camping</string>
<string>Tornado</string>
<string>Billiards</string>
<string>Rugby</string>
<string>Airshow</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>insertAlbum</key>
<array/>
<key>insertAsset</key>
<array/>
<key>insertHighlight</key>
<array/>
<key>insertMemory</key>
<array/>
<key>insertMoment</key>
<array/>
<key>removeAlbum</key>
<array/>
<key>removeAsset</key>
<array/>
<key>removeHighlight</key>
<array/>
<key>removeMemory</key>
<array/>
<key>removeMoment</key>
<array/>
</dict>
</plist>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>embeddingVersion</key>
<string>1</string>
<key>localeIdentifier</key>
<string>en_US</string>
<key>sceneTaxonomySHA</key>
<string>87914a047c69fbe8013fad2c70fa70c6c03b08b56190fe4054c880e6b9f57cc3</string>
<key>searchIndexVersion</key>
<string>10</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
</dict>
</plist>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2021-10-09T17:36:25Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2021-10-09T17:36:25Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2021-10-09T17:36:25Z</date>
<key>BackgroundJobSearch</key>
<date>2021-10-09T17:36:25Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2021-10-09T17:36:24Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2021-10-07T03:30:30Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2021-10-07T03:30:29Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2021-10-07T03:30:29Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2021-10-09T17:36:25Z</date>
<key>SiriPortraitDonation</key>
<date>2021-10-08T20:51:05Z</date>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>revgeoprovider</key>
<string>7618</string>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PLLibraryServicesManager.LocaleIdentifier</key>
<string>en_US</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

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