Feature timewarp (#675)
* Implemented timewarp command * Updated docs * Added missing pytest mark
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
113
docs/cli.html
@ -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"><DATE></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"><DELTA></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"><TIME></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"><DELTA></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"><TIMEZONE></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 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.</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 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 <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 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.</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"><ALBUM></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"><PHOTOS_LIBRARY_PATH></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"><exiftool_path></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"><output_file></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"><THEME></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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
BIN
docs/objects.inv
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -569,3 +569,4 @@ def check_version():
|
||||
"to suppress this message and prevent osxphotos from checking for latest version.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
|
||||
@ -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
@ -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()
|
||||
@ -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
@ -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,
|
||||
)
|
||||
@ -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")
|
||||
|
||||
|
||||
336
osxphotos/exif_datetime_updater.py
Normal 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
|
||||
)
|
||||
@ -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
@ -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
@ -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
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
3
setup.py
@ -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",
|
||||
|
||||
@ -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>
|
||||
BIN
tests/TestTimeWarp-10.15.7.photoslibrary/database/Photos.sqlite
Normal 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>
|
||||
BIN
tests/TestTimeWarp-10.15.7.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/TestTimeWarp-10.15.7.photoslibrary/database/photos.db
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
After Width: | Height: | Size: 1007 B |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 420 KiB |