Compare commits

...

24 Commits

Author SHA1 Message Date
Rhet Turnbull
1391675a3a Updated tests and docs 2021-12-31 20:31:15 -08:00
Rhet Turnbull
a3b2784f31 ImageConverter now uses generic context; #562 2021-12-31 17:34:41 -08:00
Rhet Turnbull
cbe79ee98c Updated docs [skip ci] 2021-12-31 09:45:42 -08:00
Rhet Turnbull
eb7a2988bf Updated CHANGELOG.md [skip ci] 2021-12-31 09:45:19 -08:00
Rhet Turnbull
42426b95ee Bug fix for #559 2021-12-31 09:35:12 -08:00
Rhet Turnbull
262a6f31e7 Updated CHANGELOG.md [skip ci] 2021-12-31 08:39:18 -08:00
Rhet Turnbull
04930c3644 Added --skip-uuid, --skip-uuid-from-file, #563 2021-12-31 08:35:26 -08:00
Rhet Turnbull
44594a8e43 Added support for projects, implements #559 2021-12-31 07:30:20 -08:00
Rhet Turnbull
690d981f31 Fixed test for #561 2021-12-30 15:45:32 -08:00
Rhet Turnbull
06ea8d1e6c Updated CHANGELOG.md [skip ci] 2021-12-29 11:24:29 -08:00
Rhet Turnbull
c4e3c5a8be Updated docs [skip ci] 2021-12-28 17:37:00 -08:00
Rhet Turnbull
03f4e7cc34 Fix for accented characters in album names, #561 2021-12-28 17:25:50 -08:00
Rhet Turnbull
0e54a08ae0 Updated docs [skip ci] 2021-12-26 20:09:10 -08:00
Rhet Turnbull
b71c752e9d Added get_photos_library_version 2021-12-26 19:57:33 -08:00
Rhet Turnbull
521848f955 Added export test for --exif 2021-12-25 05:53:26 -08:00
Rhet Turnbull
debb17c952 Implement #323 2021-12-25 05:41:37 -08:00
Rhet Turnbull
7819740f70 Fixed #463 2021-12-24 18:12:02 -08:00
Rhet Turnbull
b9ffb0d8de Fixed helped text, #493 2021-12-24 18:08:18 -08:00
Rhet Turnbull
d59852f594 Updated tests 2021-12-24 17:21:13 -08:00
Rhet Turnbull
085f482820 Added install/uninstall commands, #531 2021-12-24 17:05:01 -08:00
Rhet Turnbull
1cb8da96ce Updated dev_requirements.txt 2021-12-22 19:08:25 -08:00
Rhet Turnbull
50016a9eca Updated requirements_dev.txt 2021-12-22 18:10:15 -08:00
Rhet Turnbull
924f7325b4 Removed redundant dev_requirements.txt 2021-12-22 18:08:44 -08:00
Rhet Turnbull
181f678d9e Updated docs [skip ci] 2021-12-22 08:22:02 -08:00
241 changed files with 1674 additions and 163 deletions

View File

@@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.44.2](https://github.com/RhetTbull/osxphotos/compare/v0.44.1...v0.44.2)
> 31 December 2021
- Bug fix for #559 [`42426b9`](https://github.com/RhetTbull/osxphotos/commit/42426b95ee786b2d53482d3d931a0b962a4db20d)
#### [v0.44.1](https://github.com/RhetTbull/osxphotos/compare/v0.44.0...v0.44.1)
> 31 December 2021
- Added --skip-uuid, --skip-uuid-from-file, #563 [`04930c3`](https://github.com/RhetTbull/osxphotos/commit/04930c3644da99c1923c4e3aaa9213902aeadfd1)
#### [v0.44.0](https://github.com/RhetTbull/osxphotos/compare/v0.43.9...v0.44.0)
> 31 December 2021
- Added support for projects, implements #559 [`44594a8`](https://github.com/RhetTbull/osxphotos/commit/44594a8e437c20bae6fd8eecb74075d49da4b91f)
- Updated docs [skip ci] [`c4e3c5a`](https://github.com/RhetTbull/osxphotos/commit/c4e3c5a8beac1db00533f7820ab8249cf351aef0)
- Fixed test for #561 [`690d981`](https://github.com/RhetTbull/osxphotos/commit/690d981f310b083f5f58407cc879bca494730765)
#### [v0.43.9](https://github.com/RhetTbull/osxphotos/compare/v0.43.8...v0.43.9)
> 28 December 2021
- Fix for accented characters in album names, #561 [`03f4e7c`](https://github.com/RhetTbull/osxphotos/commit/03f4e7cc3473c276dfd7c7e6ad64e4dfe5b32011)
#### [v0.43.8](https://github.com/RhetTbull/osxphotos/compare/v0.43.7...v0.43.8)
> 26 December 2021
- Fixed #463 [`#463`](https://github.com/RhetTbull/osxphotos/issues/463)
- Updated docs [skip ci] [`181f678`](https://github.com/RhetTbull/osxphotos/commit/181f678d9eda8bc8acca11b4ebd470900f30bdcb)
- Added install/uninstall commands, #531 [`085f482`](https://github.com/RhetTbull/osxphotos/commit/085f482820af2d51f0d411c7e8a7a27329bf0722)
- Implement #323 [`debb17c`](https://github.com/RhetTbull/osxphotos/commit/debb17c9520bec25d725426feaa512745e9d4ec0)
- Updated docs [skip ci] [`0e54a08`](https://github.com/RhetTbull/osxphotos/commit/0e54a08ae07853c4cdb2c548bdba27335cfc32ba)
- Added get_photos_library_version [`b71c752`](https://github.com/RhetTbull/osxphotos/commit/b71c752e9d2c59412baf812bfc50e6358ea3f02e)
#### [v0.43.7](https://github.com/RhetTbull/osxphotos/compare/v0.43.6...v0.43.7)
> 21 December 2021
- Adds missing f-string to retry message [`#553`](https://github.com/RhetTbull/osxphotos/pull/553)
- Update issue templates [`e7bd80e`](https://github.com/RhetTbull/osxphotos/commit/e7bd80e05f94238fd41e478e32c1709b442eb361)
- Partial fix for #556 [`a08a653`](https://github.com/RhetTbull/osxphotos/commit/a08a653f202a49853780ab4a686bf3dfbc32a491)
- Updated all-contributors [`e1f1772`](https://github.com/RhetTbull/osxphotos/commit/e1f1772080d24373ceb5791683615451cd390874)
- Version bump [`6ce1b83`](https://github.com/RhetTbull/osxphotos/commit/6ce1b83ca2c7f0c6f9c86757602b81df1d9bf453)
#### [v0.43.6](https://github.com/RhetTbull/osxphotos/compare/v0.43.5...v0.43.6) #### [v0.43.6](https://github.com/RhetTbull/osxphotos/compare/v0.43.5...v0.43.6)
> 10 December 2021 > 10 December 2021

112
README.md
View File

@@ -14,7 +14,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
# Table of Contents # Table of Contents
* [Supported operating systems](#supported-operating-systems) * [Supported operating systems](#supported-operating-systems)
* [Installation instructions](#installation-instructions) * [Installation](#installation)
* [Command Line Usage](#command-line-usage) * [Command Line Usage](#command-line-usage)
+ [Command line examples](#command-line-examples) + [Command line examples](#command-line-examples)
+ [Tutorial](#tutorial) + [Tutorial](#tutorial)
@@ -25,6 +25,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
+ [ExifInfo](#exifinfo) + [ExifInfo](#exifinfo)
+ [AlbumInfo](#albuminfo) + [AlbumInfo](#albuminfo)
+ [ImportInfo](#importinfo) + [ImportInfo](#importinfo)
+ [ProjectInfo](#projectinfo)
+ [FolderInfo](#folderinfo) + [FolderInfo](#folderinfo)
+ [PlaceInfo](#placeinfo) + [PlaceInfo](#placeinfo)
+ [ScoreInfo](#scoreinfo) + [ScoreInfo](#scoreinfo)
@@ -139,20 +140,22 @@ Options:
-h, --help Show this message and exit. -h, --help Show this message and exit.
Commands: Commands:
about Print information about osxphotos including license. about Print information about osxphotos including license.
albums Print out albums found in the Photos library. albums Print out albums found in the Photos library.
dump Print list of all photos & associated info from the Photos... dump Print list of all photos & associated info from the Photos...
export Export photos from the Photos database. export Export photos from the Photos database.
help Print help; for help on commands: help <command>. help Print help; for help on commands: help <command>.
info Print out descriptive info of the Photos library database. info Print out descriptive info of the Photos library database.
keywords Print out keywords found in the Photos library. install Install Python packages into the same environment as osxphotos
labels Print out image classification labels found in the Photos... keywords Print out keywords found in the Photos library.
list Print list of Photos libraries found on the system. labels Print out image classification labels found in the Photos...
persons Print out persons (faces) found in the Photos library. list Print list of Photos libraries found on the system.
places Print out places found in the Photos library. persons Print out persons (faces) found in the Photos library.
query Query the Photos database using 1 or more search options; if... places Print out places found in the Photos library.
repl Run interactive osxphotos shell query Query the Photos database using 1 or more search options; if...
tutorial Display osxphotos tutorial. repl Run interactive osxphotos REPL shell (useful for debugging,...
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
``` ```
To get help on a specific command, use `osxphotos help <command_name>` To get help on a specific command, use `osxphotos help <command_name>`
@@ -612,7 +615,8 @@ Options:
FILENAME. If more than one --name options is FILENAME. If more than one --name options is
specified, they are treated as "OR", e.g. find specified, they are treated as "OR", e.g. find
photos matching any FILENAME. photos matching any FILENAME.
--uuid UUID Search for photos with UUID(s). --uuid UUID Search for photos with UUID(s). May be
repeated to include multiple UUIDs.
--uuid-from-file FILE Search for photos with UUID(s) loaded from --uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines FILE. Format is a single UUID per line. Lines
preceded with # are ignored. preceded with # are ignored.
@@ -729,6 +733,15 @@ Options:
repeating '--regex' with different arguments. repeating '--regex' with different arguments.
--selected Filter for photos that are currently selected --selected Filter for photos that are currently selected
in Photos. in Photos.
--exif EXIF_TAG VALUE Search for photos where EXIF_TAG exists in
photo's EXIF data and contains VALUE. For
example, to find photos created by Adobe
Photoshop: `--exif Software 'Adobe Photoshop'
`or to find all photos shot on a Canon camera:
`--exif Make Canon`. EXIF_TAG can be any valid
exiftool tag, with or without group name, e.g.
`EXIF:Make` or `Make`. To use --exif, exiftool
must be installed and in the path.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA --query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following will be evaluated in context of the following
python list comprehension: `photos = [photo python list comprehension: `photos = [photo
@@ -825,6 +838,11 @@ Options:
photos if the RAW photo does not have an photos if the RAW photo does not have an
associated JPEG image (e.g. the RAW file was associated JPEG image (e.g. the RAW file was
imported to Photos without a JPEG preview). imported to Photos without a JPEG preview).
--skip-uuid UUID Skip photos with UUID(s) during export. May be
repeated to include multiple UUIDs.
--skip-uuid-from-file FILE Skip photos with UUID(s) loaded from FILE.
Format is a single UUID per line. Lines
preceded with # are ignored.
--current-name Use photo's current filename instead of --current-name Use photo's current filename instead of
original filename for export. Note: Starting original filename for export. Note: Starting
with Photos 5, all photos are renamed upon with Photos 5, all photos are renamed upon
@@ -1703,7 +1721,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline} {lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n' {crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.43.6' {osxphotos_version} The osxphotos version, e.g. '0.44.3'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified for
@@ -1718,6 +1736,13 @@ Substitution Description
{folder_album} Folder path + album photo is contained in. e.g. {folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no 'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder enclosing folder
{project} Project(s) photo is contained in (such as greeting
cards, calendars, slideshows)
{album_project} Album(s) and project(s) photo is contained in; treats
projects as regular albums
{folder_album_project} Folder path + album (includes projects as albums)
photo is contained in. e.g. 'Folder/Subfolder/Album'
or just 'Album' if no enclosing folder
{keyword} Keyword(s) assigned to photo {keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo {person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo {label} Image categorization label associated with a photo
@@ -1862,13 +1887,13 @@ Both the '{shell_quote}' template and the '|shell_quote' template filter are
available for this purpose. For example, the following command outputs the full available for this purpose. For example, the following command outputs the full
path of newly exported files to file 'new.txt': path of newly exported files to file 'new.txt':
--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}" --post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"
In the above command, the 'shell_quote' filter is used to ensure In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is
'{filepath.name}' is properly quoted and the '{shell_quote}' template ensures properly quoted and the '{shell_quote}' template ensures the constructed path of
the constructed path of '{exported_dir}/exported.txt' is properly quoted. If '{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is 'IMG
'{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus
Export', the command thus renders to: renders to:
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt' echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
@@ -2092,7 +2117,7 @@ keywords = photosdb.keywords
Returns a list of the keywords found in the Photos library Returns a list of the keywords found in the Photos library
#### `album_info` #### <a name="photosdbalbuminfo">`album_info`</a>
```python ```python
# assumes photosdb is a PhotosDB object (see above) # assumes photosdb is a PhotosDB object (see above)
albums = photosdb.album_info albums = photosdb.album_info
@@ -2122,6 +2147,10 @@ Returns list of shared album names found in photos database (e.g. albums shared
Returns a list of [ImportInfo](#importinfo) objects representing the import sessions for the database. Returns a list of [ImportInfo](#importinfo) objects representing the import sessions for the database.
#### `project_info`
Returns a list of [ProjectInfo](#projectinfo) objects representing the projects/creations (cards, calendars, etc.) in the database.
#### `folder_info` #### `folder_info`
```python ```python
# assumes photosdb is a PhotosDB object (see above) # assumes photosdb is a PhotosDB object (see above)
@@ -2411,11 +2440,15 @@ Returns a list of keywords (e.g. tags) applied to the photo
Returns a list of albums the photo is contained in. See also [album_info](#album_info). Returns a list of albums the photo is contained in. See also [album_info](#album_info).
#### `album_info` #### `album_info`
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in. See also [albums](#albums). Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in or empty list of the photo is not in any albums. See also [albums](#albums).
#### `import_info` #### `import_info`
Returns an [ImportInfo](#importinfo) object representing the import session associated with the photo or `None` if there is no associated import session. Returns an [ImportInfo](#importinfo) object representing the import session associated with the photo or `None` if there is no associated import session.
#### `project_info`
Returns a list of [ProjectInfo](#projectinfo) objects representing projects/creations (cards, calendars, etc.) the photo is contained in or empty list if there are no projects associated with the photo.
#### `persons` #### `persons`
Returns a list of the names of the persons in the photo Returns a list of the names of the persons in the photo
@@ -2923,6 +2956,23 @@ Returns the start date as a timezone aware datetime.datetime object for when the
#### `end_date` #### `end_date`
Returns the end date as a timezone aware datetime.datetime object for when the import session completed. Returns the end date as a timezone aware datetime.datetime object for when the import session completed.
### ProjectInfo
PhotosDB.projcet_info returns a list of ProjectInfo objects. Each ProjectInfo object represents a project in the library. PhotoInfo.project_info returns a list of ProjectInfo objects for each project the photo is contained in.
Projects (found under "My Projects" in Photos) are projects or creations such as cards, calendars, and slideshows created in Photos. osxphotos provides only very basic information about projects and projects created with third party plugins may not accessible to osxphotos.
#### `uuid`
Returns the universally unique identifier (uuid) of the project. This is how Photos keeps track of individual objects within the database.
#### `title`
Returns the title or name of the project.
#### <a name="projectphotos">`photos`</a>
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the project.
#### `creation_date`
Returns the creation date as a timezone aware datetime.datetime object of the project.
### FolderInfo ### FolderInfo
PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library. PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
@@ -3573,10 +3623,13 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}| |{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'| |{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.43.6'| |{osxphotos_version}|The osxphotos version, e.g. '0.44.3'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|{project}|Project(s) photo is contained in (such as greeting cards, calendars, slideshows)|
|{album_project}|Album(s) and project(s) photo is contained in; treats projects as regular albums|
|{folder_album_project}|Folder path + album (includes projects as albums) photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|{keyword}|Keyword(s) assigned to photo| |{keyword}|Keyword(s) assigned to photo|
|{person}|Person(s) / face(s) in a photo| |{person}|Person(s) / face(s) in a photo|
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.| |{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
@@ -3678,13 +3731,6 @@ Returns path to last opened Photo Library as string.
Returns list of Photos libraries found on the system. **Note**: On MacOS 10.15, this appears to list all libraries. On older systems, it may not find some libraries if they are not located in ~/Pictures. Provided for convenience but do not rely on this to find all libraries on the system. Returns list of Photos libraries found on the system. **Note**: On MacOS 10.15, this appears to list all libraries. On older systems, it may not find some libraries if they are not located in ~/Pictures. Provided for convenience but do not rely on this to find all libraries on the system.
#### `dd_to_dms_str(lat, lon)`
Convert latitude, longitude in degrees to degrees, minutes, seconds as string.
- `lat`: latitude in degrees
- `lon`: longitude in degrees
returns: string tuple in format ("51 deg 30' 12.86\\" N", "0 deg 7' 54.50\\" W")
This is the same format used by exiftool's json format.
## Examples ## Examples

View File

@@ -1,7 +1,10 @@
build
m2r2
pyinstaller==4.4
pytest-mock
pytest==6.2.4 pytest==6.2.4
pytest-mock==3.6.1 sphinx_click
Sphinx==4.0.2 sphinx_rtd_theme
sphinx-rtd-theme==0.5.2 twine
wheel==0.36.2 wheel
twine==3.4.1 Sphinx
pyinstaller==4.3

View File

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

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.43.6 documentation</title> <title>Overview: module code &#8212; osxphotos 0.43.9 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script> <script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
@@ -93,7 +93,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.43.6 documentation</title> <title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.43.8 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script> <script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -45,6 +45,7 @@
<span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">tempfile</span> <span class="kn">import</span> <span class="nn">tempfile</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span> <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
<span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span> <span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">pprint</span> <span class="kn">import</span> <span class="n">pformat</span> <span class="kn">from</span> <span class="nn">pprint</span> <span class="kn">import</span> <span class="n">pformat</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span> <span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
@@ -3503,6 +3504,34 @@
<span class="c1"># selection only works if photos selected in main media browser</span> <span class="c1"># selection only works if photos selected in main media browser</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">photos</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
<span class="n">matching_photos</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">exifdata</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">exifdata</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">tag_groups</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
<span class="k">for</span> <span class="n">exiftag</span><span class="p">,</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
<span class="n">exifvalue</span> <span class="o">=</span> <span class="n">exifvalue</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">exiftag</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata_value</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="n">Iterable</span><span class="p">):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">:</span>
<span class="n">matching_photos</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">exiftag</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="p">(</span><span class="nb">str</span><span class="p">,</span> <span class="n">Iterable</span><span class="p">)):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">:</span>
<span class="n">matching_photos</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">matching_photos</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span> <span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
<span class="k">for</span> <span class="n">function</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span> <span class="k">for</span> <span class="n">function</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">function</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">photos</span><span class="p">)</span> <span class="n">photos</span> <span class="o">=</span> <span class="n">function</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">photos</span><span class="p">)</span>
@@ -3630,7 +3659,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

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

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.43.6 documentation</title> <title>osxphotos command line interface (CLI) &#8212; osxphotos 0.44.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.43.6 documentation</title> <title>Index &#8212; osxphotos 0.44.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.43.6 documentation</title> <title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.44.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos &#8212; osxphotos 0.43.6 documentation</title> <title>osxphotos &#8212; osxphotos 0.44.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos package &#8212; osxphotos 0.43.6 documentation</title> <title>osxphotos package &#8212; osxphotos 0.44.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
# Photos 6 (10.16.0 Beta) == 14104 # Photos 6 (10.16.0 Beta) == 14104
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"] _TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
_PHOTOS_2_VERSION = "2622"
# only version 3 - 4 have RKVersion.selfPortrait # only version 3 - 4 have RKVersion.selfPortrait
_PHOTOS_3_VERSION = "3301" _PHOTOS_3_VERSION = "3301"
@@ -121,12 +123,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
# Constants used for processing folders and albums # Constants used for processing folders and albums
_PHOTOS_5_ALBUM_KIND = 2 # normal user album _PHOTOS_5_ALBUM_KIND = 2 # normal user album
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album _PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
_PHOTOS_5_PROJECT_ALBUM_KIND = 1508 # My Projects (e.g. Calendar, Card, Slideshow)
_PHOTOS_5_FOLDER_KIND = 4000 # user folder _PHOTOS_5_FOLDER_KIND = 4000 # user folder
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder _PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass _PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums" _PHOTOS_4_ALBUM_TYPE_ALBUM = 1 # RKAlbum.albumType
_PHOTOS_4_ALBUM_TYPE_PROJECT = 9 # RKAlbum.albumType
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW = 8 # RKAlbum.albumType
_PHOTOS_4_TOP_LEVEL_ALBUMS = [
"TopLevelAlbums",
"TopLevelKeepsakes",
"TopLevelSlideshows",
]
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder" _PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
# EXIF related constants # EXIF related constants

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.43.7" __version__ = "0.44.3"

View File

@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
from ._constants import ( from ._constants import (
_PHOTOS_4_ALBUM_KIND, _PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM, _PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_VERSION, _PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND, _PHOTOS_5_FOLDER_KIND,
@@ -161,7 +161,6 @@ class AlbumInfoBaseClass:
class AlbumInfo(AlbumInfoBaseClass): class AlbumInfo(AlbumInfoBaseClass):
""" """
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album Info about a specific Album, contains all the details about the album
including folders, photos, etc. including folders, photos, etc.
""" """
@@ -231,7 +230,7 @@ class AlbumInfo(AlbumInfoBaseClass):
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"] parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
self._parent = ( self._parent = (
FolderInfo(db=self._db, uuid=parent_uuid) FolderInfo(db=self._db, uuid=parent_uuid)
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
else None else None
) )
else: else:
@@ -266,18 +265,17 @@ class AlbumInfo(AlbumInfoBaseClass):
def photo_index(self, photo): def photo_index(self, photo):
"""return index of photo in album (based on album sort order)""" """return index of photo in album (based on album sort order)"""
index = 0 for index, p in enumerate(self.photos):
for p in self.photos:
if p.uuid == photo.uuid: if p.uuid == photo.uuid:
return index return index
index += 1 raise ValueError(
else: f"Photo with uuid {photo.uuid} does not appear to be in this album"
raise ValueError( )
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
class ImportInfo(AlbumInfoBaseClass): class ImportInfo(AlbumInfoBaseClass):
"""Information about import sessions"""
@property @property
def photos(self): def photos(self):
"""return list of photos contained in import session""" """return list of photos contained in import session"""
@@ -296,6 +294,15 @@ class ImportInfo(AlbumInfoBaseClass):
return self._photos return self._photos
class ProjectInfo(AlbumInfo):
"""
ProjectInfo with info about projects
Projects are cards, calendars, slideshows, etc.
"""
...
class FolderInfo: class FolderInfo:
""" """
Info about a specific folder, contains all the details about the folder Info about a specific folder, contains all the details about the folder
@@ -357,7 +364,7 @@ class FolderInfo:
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"] parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
self._parent = ( self._parent = (
FolderInfo(db=self._db, uuid=parent_uuid) FolderInfo(db=self._db, uuid=parent_uuid)
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
else None else None
) )
else: else:

View File

@@ -20,6 +20,7 @@ import photoscript
import rich.traceback import rich.traceback
import yaml import yaml
from rich import pretty from rich import pretty
from runpy import run_module
import osxphotos import osxphotos
@@ -62,7 +63,7 @@ from .phototemplate import PhotoTemplate, RenderOptions
from .pyrepl import embed_repl from .pyrepl import embed_repl
from .queryoptions import QueryOptions from .queryoptions import QueryOptions
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import expand_and_validate_filepath, load_function from .utils import expand_and_validate_filepath, load_function, normalize_fs_path
# global variable to control verbose output # global variable to control verbose output
# set via --verbose/-V # set via --verbose/-V
@@ -297,7 +298,8 @@ def QUERY_OPTIONS(f):
metavar="UUID", metavar="UUID",
default=None, default=None,
multiple=True, multiple=True,
help="Search for photos with UUID(s).", help="Search for photos with UUID(s). "
"May be repeated to include multiple UUIDs.",
), ),
o( o(
"--uuid-from-file", "--uuid-from-file",
@@ -542,6 +544,17 @@ def QUERY_OPTIONS(f):
is_flag=True, is_flag=True,
help="Filter for photos that are currently selected in Photos.", help="Filter for photos that are currently selected in Photos.",
), ),
o(
"--exif",
metavar="EXIF_TAG VALUE",
nargs=2,
multiple=True,
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
"To use --exif, exiftool must be installed and in the path.",
),
o( o(
"--query-eval", "--query-eval",
metavar="CRITERIA", metavar="CRITERIA",
@@ -687,6 +700,23 @@ def cli(ctx, db, json_, debug):
"Note: this does not skip RAW photos if the RAW photo does not have an associated JPEG image " "Note: this does not skip RAW photos if the RAW photo does not have an associated JPEG image "
"(e.g. the RAW file was imported to Photos without a JPEG preview).", "(e.g. the RAW file was imported to Photos without a JPEG preview).",
) )
@click.option(
"--skip-uuid",
metavar="UUID",
default=None,
multiple=True,
help="Skip photos with UUID(s) during export. "
"May be repeated to include multiple UUIDs.",
)
@click.option(
"--skip-uuid-from-file",
metavar="FILE",
default=None,
multiple=False,
help="Skip photos with UUID(s) loaded from FILE. "
"Format is a single UUID per line. Lines preceded with # are ignored.",
type=click.Path(exists=True),
)
@click.option( @click.option(
"--current-name", "--current-name",
is_flag=True, is_flag=True,
@@ -1112,6 +1142,8 @@ def export(
skip_bursts, skip_bursts,
skip_live, skip_live,
skip_raw, skip_raw,
skip_uuid,
skip_uuid_from_file,
person_keyword, person_keyword,
album_keyword, album_keyword,
keyword_template, keyword_template,
@@ -1189,6 +1221,7 @@ def export(
max_size, max_size,
regex, regex,
selected, selected,
exif,
query_eval, query_eval,
query_function, query_function,
duplicate, duplicate,
@@ -1279,6 +1312,8 @@ def export(
skip_bursts = cfg.skip_bursts skip_bursts = cfg.skip_bursts
skip_live = cfg.skip_live skip_live = cfg.skip_live
skip_raw = cfg.skip_raw skip_raw = cfg.skip_raw
skip_uuid = cfg.skip_uuid
skip_uuid_from_file = cfg.skip_uuid_from_file
person_keyword = cfg.person_keyword person_keyword = cfg.person_keyword
album_keyword = cfg.album_keyword album_keyword = cfg.album_keyword
keyword_template = cfg.keyword_template keyword_template = cfg.keyword_template
@@ -1353,6 +1388,7 @@ def export(
max_size = cfg.max_size max_size = cfg.max_size
regex = cfg.regex regex = cfg.regex
selected = cfg.selected selected = cfg.selected
exif = cfg.exif
query_eval = cfg.query_eval query_eval = cfg.query_eval
query_function = cfg.query_function query_function = cfg.query_function
duplicate = cfg.duplicate duplicate = cfg.duplicate
@@ -1672,6 +1708,7 @@ def export(
max_size=max_size, max_size=max_size,
regex=regex, regex=regex,
selected=selected, selected=selected,
exif=exif,
query_eval=query_eval, query_eval=query_eval,
function=query_function, function=query_function,
duplicate=duplicate, duplicate=duplicate,
@@ -1688,6 +1725,13 @@ def export(
else: else:
raise ValueError(e) raise ValueError(e)
if skip_uuid:
photos = [p for p in photos if p.uuid not in skip_uuid]
if skip_uuid_from_file:
skip_uuid_list = load_uuid_from_file(skip_uuid_from_file)
photos = [p for p in photos if p.uuid not in skip_uuid_list]
if photos and only_new: if photos and only_new:
# ignore previously exported files # ignore previously exported files
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()} previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
@@ -2084,6 +2128,7 @@ def query(
max_size, max_size,
regex, regex,
selected, selected,
exif,
query_eval, query_eval,
query_function, query_function,
add_to_album, add_to_album,
@@ -2119,6 +2164,7 @@ def query(
max_size, max_size,
regex, regex,
selected, selected,
exif,
duplicate, duplicate,
] ]
exclusive = [ exclusive = [
@@ -2250,6 +2296,7 @@ def query(
function=query_function, function=query_function,
regex=regex, regex=regex,
selected=selected, selected=selected,
exif=exif,
duplicate=duplicate, duplicate=duplicate,
) )
@@ -3358,11 +3405,13 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
Returns: Returns:
tuple of (list of files deleted, list of directories deleted) tuple of (list of files deleted, list of directories deleted)
""" """
keepers = {str(filename).lower(): 1 for filename in files_to_keep} keepers = {
normalize_fs_path(str(filename).lower()): 1 for filename in files_to_keep
}
deleted_files = [] deleted_files = []
for p in pathlib.Path(dest_path).rglob("*"): for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower() path = normalize_fs_path(str(p).lower())
if p.is_file() and path not in keepers: if p.is_file() and path not in keepers:
verbose_(f"Deleting {p}") verbose_(f"Deleting {p}")
fileutil.unlink(p) fileutil.unlink(p)
@@ -3602,6 +3651,30 @@ def run_post_command(
) )
@cli.command()
@click.argument("packages", nargs=-1, required=True)
@click.option(
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
)
def install(packages, upgrade):
"""Install Python packages into the same environment as osxphotos"""
args = ["pip", "install"]
if upgrade:
args += ["--upgrade"]
args += list(packages)
sys.argv = args
run_module("pip", run_name="__main__")
@cli.command()
@click.argument("packages", nargs=-1, required=True)
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
def uninstall(packages, yes):
"""Uninstall Python packages from the osxphotos environment"""
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
run_module("pip", run_name="__main__")
@cli.command(hidden=True) @cli.command(hidden=True)
@DB_OPTION @DB_OPTION
@DB_ARGUMENT @DB_ARGUMENT
@@ -3615,7 +3688,8 @@ def run_post_command(
@click.option( @click.option(
"--uuid", "--uuid",
metavar="UUID", metavar="UUID",
help="Use with '--dump photos' to dump only certain UUIDs", help="Use with '--dump photos' to dump only certain UUIDs. "
"May be repeated to include multiple UUIDs.",
multiple=True, multiple=True,
) )
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.") @click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@@ -4123,6 +4197,7 @@ def _spotlight_photo(photo: PhotoInfo):
) )
def repl(ctx, cli_obj, db, emacs): def repl(ctx, cli_obj, db, emacs):
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)""" """Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
import logging
from objexplore import explore from objexplore import explore
from photoscript import Album, Photo, PhotosLibrary from photoscript import Album, Photo, PhotosLibrary
@@ -4133,6 +4208,9 @@ def repl(ctx, cli_obj, db, emacs):
from osxphotos.placeinfo import PlaceInfo from osxphotos.placeinfo import PlaceInfo
from osxphotos.queryoptions import QueryOptions from osxphotos.queryoptions import QueryOptions
logger = logging.getLogger()
logger.disabled = True
pretty.install() pretty.install()
print(f"python version: {sys.version}") print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}") print(f"osxphotos version: {osxphotos._version.__version__}")

View File

@@ -224,13 +224,13 @@ The following attributes may be used with '--xattr-template':
) )
formatter.write("\n") formatter.write("\n")
formatter.write( formatter.write(
'--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"' '--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
) )
formatter.write("\n\n") formatter.write("\n\n")
formatter.write_text( formatter.write_text(
"In the above command, the 'shell_quote' filter is used to ensure '{filepath.name}' is properly quoted " "In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is properly quoted "
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. " + "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
"If '{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command " "If '{filepath}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
"thus renders to: " "thus renders to: "
) )
formatter.write("\n") formatter.write("\n")

View File

@@ -17,25 +17,25 @@ from wurlitzer import pipes
class ImageConversionError(Exception): class ImageConversionError(Exception):
"""Base class for exceptions in this module. """ """Base class for exceptions in this module."""
pass pass
class ImageConverter: class ImageConverter:
""" Convert images to jpeg. This class is a singleton """Convert images to jpeg. This class is a singleton
which will re-use the Core Image CIContext to avoid which will re-use the Core Image CIContext to avoid
creating a new context for every conversion. """ creating a new context for every conversion."""
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """ """create new object or return instance of already created singleton"""
if not hasattr(cls, "instance") or not cls.instance: if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls) cls.instance = super().__new__(cls)
return cls.instance return cls.instance
def __init__(self): def __init__(self):
""" return existing singleton or create a new one """ """return existing singleton or create a new one"""
if hasattr(self, "context"): if hasattr(self, "context"):
return return
@@ -47,13 +47,10 @@ class ImageConverter:
"workingFormat": Quartz.kCIFormatRGBAh, "workingFormat": Quartz.kCIFormatRGBAh,
} }
) )
mtldevice = Metal.MTLCreateSystemDefaultDevice() self.context = Quartz.CIContext.contextWithOptions_(context_options)
self.context = Quartz.CIContext.contextWithMTLDevice_options_(
mtldevice, context_options
)
def write_jpeg(self, input_path, output_path, compression_quality=1.0): def write_jpeg(self, input_path, output_path, compression_quality=1.0):
""" convert image to jpeg and write image to output_path """convert image to jpeg and write image to output_path
Args: Args:
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
@@ -104,8 +101,11 @@ class ImageConverter:
if input_image is None: if input_image is None:
raise ImageConversionError(f"Could not create CIImage for {input_path}") raise ImageConversionError(f"Could not create CIImage for {input_path}")
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName( output_colorspace = (
Quartz.CoreGraphics.kCGColorSpaceSRGB input_image.colorSpace()
or Quartz.CGColorSpaceCreateWithName(
Quartz.CoreGraphics.kCGColorSpaceSRGB
)
) )
output_options = NSDictionary.dictionaryWithDictionary_( output_options = NSDictionary.dictionaryWithDictionary_(
@@ -123,4 +123,3 @@ class ImageConverter:
raise ImageConversionError( raise ImageConversionError(
f"Error converting file {input_path} to jpeg at {output_path}: {error}" f"Error converting file {input_path} to jpeg at {output_path}: {error}"
) )

View File

@@ -20,10 +20,14 @@ from .._constants import (
_MOVIE_TYPE, _MOVIE_TYPE,
_PHOTO_TYPE, _PHOTO_TYPE,
_PHOTOS_4_ALBUM_KIND, _PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_ROOT_FOLDER, _PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_VERSION, _PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND, _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_PROJECT_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_SHARED_ALBUM_KIND,
_PHOTOS_5_SHARED_PHOTO_PATH, _PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION, _PHOTOS_5_VERSION,
@@ -34,7 +38,7 @@ from .._constants import (
TEXT_DETECTION_CONFIDENCE_THRESHOLD, TEXT_DETECTION_CONFIDENCE_THRESHOLD,
) )
from ..adjustmentsinfo import AdjustmentsInfo from ..adjustmentsinfo import AdjustmentsInfo
from ..albuminfo import AlbumInfo, ImportInfo from ..albuminfo import AlbumInfo, ImportInfo, ProjectInfo
from ..momentinfo import MomentInfo from ..momentinfo import MomentInfo
from ..personinfo import FaceInfo, PersonInfo from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate, RenderOptions from ..phototemplate import PhotoTemplate, RenderOptions
@@ -570,6 +574,18 @@ class PhotoInfo:
) )
return self._import_info return self._import_info
@property
def project_info(self):
"""list of AlbumInfo objects representing projects for the photo or None if no projects"""
try:
return self._project_info
except AttributeError:
project_uuids = self._get_album_uuids(project=True)
self._project_info = [
ProjectInfo(db=self._db, uuid=album) for album in project_uuids
]
return self._project_info
@property @property
def keywords(self): def keywords(self):
"""list of keywords for picture""" """list of keywords for picture"""
@@ -1197,34 +1213,48 @@ class PhotoInfo:
"""Returns latitude, in degrees""" """Returns latitude, in degrees"""
return self._info["latitude"] return self._info["latitude"]
def _get_album_uuids(self): def _get_album_uuids(self, project=False):
"""Return list of album UUIDs this photo is found in """Return list of album UUIDs this photo is found in
Filters out albums in the trash and any special album types Filters out albums in the trash and any special album types
if project is True, returns special "My Project" albums (e.g. cards, calendars, slideshows)
Returns: list of album UUIDs Returns: list of album UUIDs
""" """
if self._db._db_version <= _PHOTOS_4_VERSION: if self._db._db_version <= _PHOTOS_4_VERSION:
version4 = True
album_kind = [_PHOTOS_4_ALBUM_KIND] album_kind = [_PHOTOS_4_ALBUM_KIND]
else: album_type = (
version4 = False [_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW]
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND] if project
else [_PHOTOS_4_ALBUM_TYPE_ALBUM]
)
album_list = []
for album in self._info["albums"]:
detail = self._db._dbalbum_details[album]
if (
detail["kind"] in album_kind
and detail["albumType"] in album_type
and not detail["intrash"]
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
):
album_list.append(album)
return album_list
# Photos 5+
album_kind = (
[_PHOTOS_5_PROJECT_ALBUM_KIND]
if project
else [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
)
album_list = [] album_list = []
for album in self._info["albums"]: for album in self._info["albums"]:
detail = self._db._dbalbum_details[album] detail = self._db._dbalbum_details[album]
if ( if detail["kind"] in album_kind and not detail["intrash"]:
detail["kind"] in album_kind
and not detail["intrash"]
and (
not version4
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
)
):
album_list.append(album) album_list.append(album)
return album_list return album_list

View File

@@ -12,6 +12,7 @@ import re
import sys import sys
import tempfile import tempfile
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Iterable
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pprint import pformat from pprint import pformat
from typing import List from typing import List
@@ -27,11 +28,15 @@ from .._constants import (
_PHOTOS_3_VERSION, _PHOTOS_3_VERSION,
_PHOTOS_4_ALBUM_KIND, _PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER, _PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUM, _PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_VERSION, _PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND, _PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND, _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_PROJECT_ALBUM_KIND,
_PHOTOS_5_ROOT_FOLDER_KIND, _PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_SHARED_ALBUM_KIND,
_TESTED_OS_VERSIONS, _TESTED_OS_VERSIONS,
@@ -41,7 +46,7 @@ from .._constants import (
TIME_DELTA, TIME_DELTA,
) )
from .._version import __version__ from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
from ..fileutil import FileUtil from ..fileutil import FileUtil
from ..personinfo import PersonInfo from ..personinfo import PersonInfo
@@ -428,7 +433,7 @@ class PhotosDB:
for folder, detail in self._dbfolder_details.items() for folder, detail in self._dbfolder_details.items()
if not detail["intrash"] if not detail["intrash"]
and not detail["isMagic"] and not detail["isMagic"]
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM and detail["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
] ]
else: else:
folders = [ folders = [
@@ -449,7 +454,7 @@ class PhotosDB:
for folder in self._dbfolder_details.values() for folder in self._dbfolder_details.values()
if not folder["intrash"] if not folder["intrash"]
and not folder["isMagic"] and not folder["isMagic"]
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM and folder["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
] ]
else: else:
folder_names = [ folder_names = [
@@ -528,6 +533,18 @@ class PhotosDB:
] ]
return self._import_info return self._import_info
@property
def project_info(self):
"""return list of AlbumInfo projects for each project in the database"""
try:
return self._project_info
except AttributeError:
self._project_info = [
ProjectInfo(db=self, uuid=album)
for album in self._get_album_uuids(project=True)
]
return self._project_info
@property @property
def db_version(self): def db_version(self):
"""return the database version as stored in LiGlobals table""" """return the database version as stored in LiGlobals table"""
@@ -847,11 +864,10 @@ class PhotosDB:
# build folder hierarchy # build folder hierarchy
for album, details in self._dbalbum_details.items(): for album, details in self._dbalbum_details.items():
parent_folder = details["folderUuid"] parent_folder = details["folderUuid"]
if details[ if (
"albumSubclass" details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
] == _PHOTOS_4_ALBUM_KIND and parent_folder not in [ and parent_folder not in _PHOTOS_4_TOP_LEVEL_ALBUMS
_PHOTOS_4_TOP_LEVEL_ALBUM ):
]:
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder) folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
self._dbalbum_folders[album] = folder_hierarchy self._dbalbum_folders[album] = folder_hierarchy
else: else:
@@ -1581,7 +1597,7 @@ class PhotosDB:
if parent_uuid is None: if parent_uuid is None:
return folders return folders
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM: if parent_uuid in _PHOTOS_4_TOP_LEVEL_ALBUMS:
if not folders: if not folders:
# this is a top-level folder with no sub-folders # this is a top-level folder with no sub-folders
folders = {uuid: None} folders = {uuid: None}
@@ -2824,7 +2840,7 @@ class PhotosDB:
hierarchy = _recurse_folder_hierarchy(folders) hierarchy = _recurse_folder_hierarchy(folders)
return hierarchy return hierarchy
def _get_album_uuids(self, shared=False, import_session=False): def _get_album_uuids(self, shared=False, import_session=False, project=False):
"""Return list of album UUIDs found in photos database """Return list of album UUIDs found in photos database
Filters out albums in the trash and any special album types Filters out albums in the trash and any special album types
@@ -2832,20 +2848,21 @@ class PhotosDB:
Args: Args:
shared: boolean; if True, returns shared albums, else normal albums shared: boolean; if True, returns shared albums, else normal albums
import_session: boolean, if True, returns import session albums, else normal or shared albums import_session: boolean, if True, returns import session albums, else normal or shared albums
project: boolean, if True, returns albums that are part of My Projects
Note: flags (shared, import_session) are mutually exclusive Note: flags (shared, import_session) are mutually exclusive
Raises: Raises:
ValueError: raised if mutually exclusive flags passed ValueError: raised if mutually exclusive flags passed
Returns: list of album UUIDs Returns: list of album UUIDs
""" """
if shared and import_session: if sum(bool(x) for x in [shared, import_session, project]) > 1:
raise ValueError( raise ValueError(
"flags are mutually exclusive: pass zero or one of shared, import_session" "flags are mutually exclusive: pass zero or one of shared, import_session, projects"
) )
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
version4 = True
if shared: if shared:
logging.warning( logging.warning(
f"Shared albums not implemented for Photos library version {self._db_version}" f"Shared albums not implemented for Photos library version {self._db_version}"
@@ -2856,16 +2873,44 @@ class PhotosDB:
f"Import sessions not implemented for Photos library version {self._db_version}" f"Import sessions not implemented for Photos library version {self._db_version}"
) )
return [] # not implemented for _PHOTOS_4_VERSION return [] # not implemented for _PHOTOS_4_VERSION
else: elif project:
album_type = [
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
]
album_kind = _PHOTOS_4_ALBUM_KIND album_kind = _PHOTOS_4_ALBUM_KIND
else:
version4 = False
if shared:
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
elif import_session:
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
else: else:
album_kind = _PHOTOS_5_ALBUM_KIND album_type = [_PHOTOS_4_ALBUM_TYPE_ALBUM]
album_kind = _PHOTOS_4_ALBUM_KIND
album_list = []
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
for album, detail in self._dbalbum_details.items():
if (
detail["kind"] == album_kind
and detail["albumType"] in album_type
and not detail["intrash"]
and (
(shared and detail["cloudownerhashedpersonid"] is not None)
or (not shared and detail["cloudownerhashedpersonid"] is None)
)
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
):
album_list.append(album)
return album_list
# Photos version 5+
if shared:
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
elif import_session:
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
elif project:
album_kind = _PHOTOS_5_PROJECT_ALBUM_KIND
else:
album_kind = _PHOTOS_5_ALBUM_KIND
album_list = [] album_list = []
# look through _dbalbum_details because _dbalbums_album won't have empty albums it # look through _dbalbum_details because _dbalbums_album won't have empty albums it
@@ -2877,13 +2922,6 @@ class PhotosDB:
(shared and detail["cloudownerhashedpersonid"] is not None) (shared and detail["cloudownerhashedpersonid"] is not None)
or (not shared and detail["cloudownerhashedpersonid"] is None) or (not shared and detail["cloudownerhashedpersonid"] is None)
) )
and (
not version4
# in Photos 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
)
): ):
album_list.append(album) album_list.append(album)
return album_list return album_list
@@ -3470,6 +3508,34 @@ class PhotosDB:
# selection only works if photos selected in main media browser # selection only works if photos selected in main media browser
photos = [] photos = []
if options.exif:
matching_photos = []
for p in photos:
if not p.exiftool:
continue
exifdata = p.exiftool.asdict(normalized=True)
exifdata.update(p.exiftool.asdict(tag_groups=False, normalized=True))
for exiftag, exifvalue in options.exif:
if options.ignore_case:
exifvalue = exifvalue.lower()
exifdata_value = exifdata.get(exiftag.lower(), "")
if isinstance(exifdata_value, str):
exifdata_value = exifdata_value.lower()
elif isinstance(exifdata_value, Iterable):
exifdata_value = [v.lower() for v in exifdata_value]
else:
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
else:
exifdata_value = exifdata.get(exiftag.lower(), "")
if not isinstance(exifdata_value, (str, Iterable)):
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
photos = matching_photos
if options.function: if options.function:
for function in options.function: for function in options.function:
photos = function[0](photos) photos = function[0](photos)

View File

@@ -4,7 +4,11 @@ import logging
import plistlib import plistlib
from .._constants import ( from .._constants import (
_PHOTOS_2_VERSION,
_PHOTOS_3_VERSION,
_PHOTOS_4_VERSION,
_PHOTOS_5_MODEL_VERSION, _PHOTOS_5_MODEL_VERSION,
_PHOTOS_5_VERSION,
_PHOTOS_6_MODEL_VERSION, _PHOTOS_6_MODEL_VERSION,
_PHOTOS_7_MODEL_VERSION, _PHOTOS_7_MODEL_VERSION,
_TESTED_DB_VERSIONS, _TESTED_DB_VERSIONS,
@@ -83,3 +87,32 @@ def get_db_model_version(db_file):
logging.warning(f"Unknown model version: {model_ver}") logging.warning(f"Unknown model version: {model_ver}")
# cross our fingers and try latest version # cross our fingers and try latest version
return 7 return 7
class UnknownLibraryVersion(Exception):
pass
def get_photos_library_version(library_path):
"""Return int indicating which Photos version a library was created with """
library_path = pathlib.Path(library_path)
db_ver = get_db_version(str(library_path / "database" / "photos.db"))
db_ver = int(db_ver)
if db_ver == int(_PHOTOS_2_VERSION):
return 2
if db_ver == int(_PHOTOS_3_VERSION):
return 3
if db_ver == int(_PHOTOS_4_VERSION):
return 4
if db_ver != int(_PHOTOS_5_VERSION):
raise UnknownLibraryVersion(f"db_ver = {db_ver}")
model_ver = get_model_version(str(library_path / "database" / "Photos.sqlite"))
model_ver = int(model_ver)
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
return 5
if _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
return 6
if _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
return 7
raise UnknownLibraryVersion(f"db_ver = {db_ver}, model_ver = {model_ver}")

View File

@@ -181,6 +181,9 @@ TEMPLATE_SUBSTITUTIONS_PATHLIB = {
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = { TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{album}": "Album(s) photo is contained in", "{album}": "Album(s) photo is contained in",
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder", "{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
"{project}": "Project(s) photo is contained in (such as greeting cards, calendars, slideshows)",
"{album_project}": "Album(s) and project(s) photo is contained in; treats projects as regular albums",
"{folder_album_project}": "Folder path + album (includes projects as albums) photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
"{keyword}": "Keyword(s) assigned to photo", "{keyword}": "Keyword(s) assigned to photo",
"{person}": "Person(s) / face(s) in a photo", "{person}": "Person(s) / face(s) in a photo",
"{label}": "Image categorization label associated with a photo (Photos 5+ only). " "{label}": "Image categorization label associated with a photo (Photos 5+ only). "
@@ -1116,6 +1119,11 @@ class PhotoTemplate:
values = [] values = []
if field == "album": if field == "album":
values = self.photo.burst_albums if self.photo.burst else self.photo.albums values = self.photo.burst_albums if self.photo.burst else self.photo.albums
elif field == "project":
values = [p.title for p in self.photo.project_info]
elif field == "album_project":
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
values += [p.title for p in self.photo.project_info]
elif field == "keyword": elif field == "keyword":
values = self.photo.keywords values = self.photo.keywords
elif field == "person": elif field == "person":
@@ -1126,13 +1134,15 @@ class PhotoTemplate:
values = self.photo.labels values = self.photo.labels
elif field == "label_normalized": elif field == "label_normalized":
values = self.photo.labels_normalized values = self.photo.labels_normalized
elif field == "folder_album": elif field in ["folder_album", "folder_album_project"]:
values = [] values = []
# photos must be in an album to be in a folder # photos must be in an album to be in a folder
if self.photo.burst: if self.photo.burst:
album_info = self.photo.burst_album_info album_info = self.photo.burst_album_info
else: else:
album_info = self.photo.album_info album_info = self.photo.album_info
if field == "folder_album_project":
album_info += self.photo.project_info
for album in album_info: for album in album_info:
if album.folder_names: if album.folder_names:
# album in folder # album in folder
@@ -1193,7 +1203,7 @@ class PhotoTemplate:
elif isinstance(obj, (str, int, float)): elif isinstance(obj, (str, int, float)):
values = [str(obj)] values = [str(obj)]
else: else:
values = [val for val in obj] values = list(obj)
elif field == "detected_text": elif field == "detected_text":
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield) values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
else: else:
@@ -1202,7 +1212,7 @@ class PhotoTemplate:
# sanitize directory names if needed, folder_album handled differently above # sanitize directory names if needed, folder_album handled differently above
if self.filename: if self.filename:
values = [sanitize_pathpart(value) for value in values] values = [sanitize_pathpart(value) for value in values]
elif self.dirname and field != "folder_album": elif self.dirname and field not in ["folder_album", "folder_album_project"]:
# skip folder_album because it would have been handled above # skip folder_album because it would have been handled above
values = [sanitize_dirname(value) for value in values] values = [sanitize_dirname(value) for value in values]

View File

@@ -84,6 +84,7 @@ class QueryOptions:
no_location: Optional[bool] = None no_location: Optional[bool] = None
function: Optional[List[Tuple[callable, str]]] = None function: Optional[List[Tuple[callable, str]]] = None
selected: Optional[bool] = None selected: Optional[bool] = None
exif: Optional[Iterable[Tuple[str, str]]] = None
def asdict(self): def asdict(self):
return asdict(self) return asdict(self)

View File

@@ -1,9 +0,0 @@
build
m2r2
pyinstaller==4.4
pytest-mock
pytest==6.2.4
sphinx_click
sphinx_rtd_theme
twine
wheel

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,12 @@
<?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>MajorVersion</key>
<integer>1</integer>
<key>MinorVersion</key>
<integer>34</integer>
<key>createDate</key>
<date>2021-12-30T03:56:43Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
<?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>DatabaseMinorVersion</key>
<integer>1</integer>
<key>DatabaseVersion</key>
<integer>112</integer>
<key>LastOpenMode</key>
<integer>2</integer>
<key>LibrarySchemaVersion</key>
<integer>2622</integer>
<key>MetaSchemaVersion</key>
<integer>2</integer>
<key>createDate</key>
<date>2021-12-29T18:15:21Z</date>
<key>databaseUuid</key>
<string>Nm7MKBmoSRygMmA9WlEaGw</string>
</dict>
</plist>

View File

@@ -0,0 +1,27 @@
<?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>LithiumMessageTracer</key>
<dict>
<key>LastReportedDate</key>
<date>2021-12-30T03:56:48Z</date>
</dict>
<key>PXPeopleScreenUnlocked</key>
<true/>
<key>Photos</key>
<dict>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>7</integer>
<key>kZoomLevelIdentifierVersions</key>
<integer>7</integer>
</dict>
</dict>
<key>RDLegacyProxyMediaRelocationCleanupCompletedKey</key>
<true/>
<key>ShowHiddenPhotosAlbumUserDefault</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2021-12-31T04:32:23Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2021-12-31T04:32:23Z</date>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PLLanguageAndLocaleKey</key>
<string>en-US:en_US</string>
<key>PLLastGeoProviderIdKey</key>
<string>7618</string>
<key>PLLastLocationInfoFormatVer</key>
<integer>12</integer>
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2021-12-30T03:56:45Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?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>LastHistoryRowId</key>
<integer>164</integer>
<key>LibraryBuildTag</key>
<string>E371079C-71D9-4C33-91F6-54B17B4B3066</string>
<key>LibrarySchemaVersion</key>
<integer>2622</integer>
</dict>
</plist>

View File

@@ -0,0 +1,47 @@
<?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>FileVersion</key>
<integer>11</integer>
<key>Source</key>
<dict>
<key>35230</key>
<dict>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>1</integer>
</dict>
<key>CurrentVersion</key>
<integer>1</integer>
<key>NoResultErrorIsSuccess</key>
<true/>
</dict>
<key>57879</key>
<dict>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>1</integer>
</dict>
<key>CurrentVersion</key>
<integer>1</integer>
<key>NoResultErrorIsSuccess</key>
<true/>
</dict>
<key>7618</key>
<dict>
<key>AddCountyIfNeeded</key>
<true/>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>10</integer>
</dict>
<key>CurrentVersion</key>
<integer>10</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?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>MajorVersion</key>
<integer>1</integer>
<key>MinorVersion</key>
<integer>34</integer>
<key>createDate</key>
<date>2021-12-30T03:56:43Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
<?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>creationDate</key>
<date>2021-12-30T03:56:00Z</date>
<key>name</key>
<string>Photos</string>
<key>previewImageHash</key>
<integer>0</integer>
<key>previewImageName</key>
<string>Calendar</string>
<key>themeIdentifier</key>
<string>Picture-Calendar</string>
<key>type</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?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>MajorVersion</key>
<integer>1</integer>
<key>MinorVersion</key>
<integer>34</integer>
<key>createDate</key>
<date>2021-12-30T03:56:43Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
<?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>creationDate</key>
<date>2021-12-30T03:55:08Z</date>
<key>name</key>
<string>Photos</string>
<key>previewImageHash</key>
<integer>0</integer>
<key>previewImageName</key>
<string>Card_Landscape</string>
<key>themeIdentifier</key>
<string>PremiumClassic-FoldedCard</string>
<key>type</key>
<integer>1</integer>
</dict>
</plist>

View File

@@ -0,0 +1,112 @@
<?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>DatabaseMinorVersion</key>
<integer>1</integer>
<key>DatabaseVersion</key>
<integer>112</integer>
<key>HistoricalMarker</key>
<dict>
<key>LastHistoryRowId</key>
<integer>164</integer>
<key>LibraryBuildTag</key>
<string>E371079C-71D9-4C33-91F6-54B17B4B3066</string>
<key>LibrarySchemaVersion</key>
<integer>2622</integer>
</dict>
<key>LibrarySchemaVersion</key>
<integer>2622</integer>
<key>MetaSchemaVersion</key>
<integer>2</integer>
<key>SnapshotComplete</key>
<true/>
<key>SnapshotCompletedDate</key>
<date>2021-12-30T03:56:44Z</date>
<key>SnapshotLastAttemptStartDate</key>
<date>2021-12-30T03:56:44Z</date>
<key>SnapshotTables</key>
<dict>
<key>RKAdminData</key>
<dict>
<key>0000000000.lisj</key>
<string>33ba9a656c3588b4bdc1e44575e794e1d256d625</string>
</dict>
<key>RKAlbum</key>
<dict>
<key>0000000000.lisj</key>
<string>3b09c246d3a74e7f57265e6ab6abb0d333665215</string>
</dict>
<key>RKAlbumVersion</key>
<dict>
<key>0000000000.lisj</key>
<string>fc7c9baa656623406304fcc10474e561dff5a53a</string>
</dict>
<key>RKBookmark</key>
<dict>
<key>0000000000.lisj</key>
<string>33e3f9220c22909667ab63c0070757b8ffe1aeac</string>
</dict>
<key>RKCustomSortOrder</key>
<dict>
<key>0000000000.lisj</key>
<string>d65281f0c3f9e519cc5a00e21bb5ece35aa03cbd</string>
</dict>
<key>RKFace</key>
<dict>
<key>0000000000.lisj</key>
<string>dd0f12962d72c2e53f483bd3e02e18f3bbf423ac</string>
</dict>
<key>RKFolder</key>
<dict>
<key>0000000000.lisj</key>
<string>8712074c0728e02c777705031c0e16882eda9bff</string>
</dict>
<key>RKImageProxyState</key>
<dict>
<key>0000000000.lisj</key>
<string>5b0f369a4df955c63b1a636980e76e3d30d99fa9</string>
</dict>
<key>RKImportGroup</key>
<dict>
<key>0000000000.lisj</key>
<string>0ad244b52516a1cac3ea5532af1e60b3f74b4af7</string>
</dict>
<key>RKKeyword</key>
<dict>
<key>0000000000.lisj</key>
<string>85669e2bf25048d655ed8042d1607d50587de1fc</string>
</dict>
<key>RKKeywordForVersion</key>
<dict>
<key>0000000000.lisj</key>
<string>cf61584fa6191e45a64311ae4617418fde4d5263</string>
</dict>
<key>RKMaster</key>
<dict>
<key>0000000000.lisj</key>
<string>a609e1942bd9e5d69f3532e1e3e091156f5c3469</string>
</dict>
<key>RKModelResource</key>
<dict>
<key>0000000000.lisj</key>
<string>c677e8613d841620d96ed750d1a720bd283a0df7</string>
</dict>
<key>RKVersion</key>
<dict>
<key>0000000000.lisj</key>
<string>b2d209286b8f7b68a5fba0fb84e364ce55d83608</string>
</dict>
<key>RKVersionAnalysisState</key>
<dict>
<key>0000000000.lisj</key>
<string>2a36598c306df54e303c14ccb19bcdeae2d85bf5</string>
</dict>
<key>RKVolume</key>
<dict>
<key>0000000000.lisj</key>
<string>5d16bc7903a060b2e879d148100441b1ac5c1629</string>
</dict>
</dict>
</dict>
</plist>

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