Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76aee7f189 | ||
|
|
147b30f973 | ||
|
|
a73dc72558 | ||
|
|
c99cf5518d | ||
|
|
1391675a3a | ||
|
|
a3b2784f31 | ||
|
|
cbe79ee98c | ||
|
|
eb7a2988bf | ||
|
|
42426b95ee | ||
|
|
262a6f31e7 | ||
|
|
04930c3644 | ||
|
|
44594a8e43 | ||
|
|
690d981f31 | ||
|
|
06ea8d1e6c | ||
|
|
c4e3c5a8be | ||
|
|
03f4e7cc34 | ||
|
|
0e54a08ae0 | ||
|
|
b71c752e9d | ||
|
|
521848f955 | ||
|
|
debb17c952 | ||
|
|
7819740f70 | ||
|
|
b9ffb0d8de | ||
|
|
d59852f594 | ||
|
|
085f482820 | ||
|
|
1cb8da96ce | ||
|
|
50016a9eca | ||
|
|
924f7325b4 | ||
|
|
181f678d9e | ||
|
|
6ce1b83ca2 | ||
|
|
a08a653f20 | ||
|
|
e1f1772080 | ||
|
|
d2a1f792e9 | ||
|
|
e7bd80e05f | ||
|
|
9089c0323c | ||
|
|
197e5663df | ||
|
|
f6dedaa619 | ||
|
|
0906dbe637 | ||
|
|
0629b3f6d6 | ||
|
|
55dfc0ec7d | ||
|
|
b7f8b26f1d | ||
|
|
9ca7dd50bc | ||
|
|
0e73d57bdf | ||
|
|
9de2c17e47 | ||
|
|
1bae6d33f1 | ||
|
|
a52b4d2f43 | ||
|
|
3e038bf124 | ||
|
|
870ed9c435 | ||
|
|
68e7ca3277 | ||
|
|
c9142c2156 | ||
|
|
7d923590ae | ||
|
|
5383ced1ca |
@@ -268,6 +268,42 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dgleich",
|
||||
"name": "David Gleich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
|
||||
"profile": "https://www.cs.purdue.edu/homes/dgleich",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alandefreitas",
|
||||
"name": "Alan de Freitas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
|
||||
"profile": "https://alandefreitas.github.io/alandefreitas/",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hyfen",
|
||||
"name": "Andrew Louis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
|
||||
"profile": "https://hyfen.net",
|
||||
"contributions": [
|
||||
"doc", "code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neebah",
|
||||
"name": "neebah",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
|
||||
"profile": "https://github.com/neebah",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
** Before submitting a bug report, please ensure you are running the most recent version of osxphotos and that the bug is reproducible on the latest version **
|
||||
|
||||
- If you installed with pipx: `pipx upgrade osxphotos`
|
||||
- If you installed with pip: `pip install --upgrade osxphotos`
|
||||
- If you installed the pre-built binary, download and install the latest [release](https://github.com/RhetTbull/osxphotos/releases)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. What' the full command line you used with osxphotos?
|
||||
2. What was the error output?
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. which version of macOS]
|
||||
- osxphotos version (`osxphotos --version`)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
80
CHANGELOG.md
@@ -4,6 +4,86 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.44.3](https://github.com/RhetTbull/osxphotos/compare/v0.44.2...v0.44.3)
|
||||
|
||||
> 31 December 2021
|
||||
|
||||
- ImageConverter now uses generic context; #562 [`a3b2784`](https://github.com/RhetTbull/osxphotos/commit/a3b2784f3177a753b78965b8ca205ca9bbb08168)
|
||||
- Updated tests and docs [`1391675`](https://github.com/RhetTbull/osxphotos/commit/1391675a3a45be0d6800a68c8bcc6d0d55d1ab7a)
|
||||
- Updated docs [skip ci] [`cbe79ee`](https://github.com/RhetTbull/osxphotos/commit/cbe79ee98cae68e0789df275220f5a5870a8bd91)
|
||||
|
||||
#### [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)
|
||||
|
||||
> 10 December 2021
|
||||
|
||||
- Fixes typo in README [`#548`](https://github.com/RhetTbull/osxphotos/pull/548)
|
||||
- docs: add alandefreitas as a contributor for bug [`#551`](https://github.com/RhetTbull/osxphotos/pull/551)
|
||||
- docs: add dgleich as a contributor for code [`#541`](https://github.com/RhetTbull/osxphotos/pull/541)
|
||||
- Updated docs [`197e566`](https://github.com/RhetTbull/osxphotos/commit/197e5663df058a013ce2d6f8c5fd7ff71a5cc46e)
|
||||
- Added test library for Monterey on M1 [`3e038bf`](https://github.com/RhetTbull/osxphotos/commit/3e038bf124b98d6b74f19dd4db0f8f1e3c48e787)
|
||||
- Updated docs [skip ci] [`f6dedaa`](https://github.com/RhetTbull/osxphotos/commit/f6dedaa6197dc244616c5b4e9e8ce42ce6b7a252)
|
||||
- Added MomentInfo for Photos 5+, #71 [`a52b4d2`](https://github.com/RhetTbull/osxphotos/commit/a52b4d2f43970086bf25659bd58dc8479b841704)
|
||||
- Fixed error for missing photo path, #547 [`0906dbe`](https://github.com/RhetTbull/osxphotos/commit/0906dbe6370922b4c9649350014ed8a21d29c4fd)
|
||||
|
||||
#### [v0.43.5](https://github.com/RhetTbull/osxphotos/compare/v0.43.4...v0.43.5)
|
||||
|
||||
> 25 November 2021
|
||||
|
||||
- Updated dependencies for pyobjc 8.0 [`7d92359`](https://github.com/RhetTbull/osxphotos/commit/7d923590ae4df941b1b9d35c21937c03eb7b4284)
|
||||
|
||||
#### [v0.43.4](https://github.com/RhetTbull/osxphotos/compare/v0.43.3...v0.43.4)
|
||||
|
||||
> 11 November 2021
|
||||
|
||||
- Fix for --use-photokit with --skip-live, #537 [`0e6c92d`](https://github.com/RhetTbull/osxphotos/commit/0e6c92dbd951dd0e63cfb8b6d64e6ab96ece5955)
|
||||
|
||||
#### [v0.43.3](https://github.com/RhetTbull/osxphotos/compare/v0.43.1...v0.43.3)
|
||||
|
||||
> 7 November 2021
|
||||
|
||||
134
README.md
@@ -5,7 +5,7 @@
|
||||

|
||||
[](https://pepy.tech/project/osxphotos)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
@@ -14,7 +14,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
# Table of Contents
|
||||
* [Supported operating systems](#supported-operating-systems)
|
||||
* [Installation instructions](#installation-instructions)
|
||||
* [Installation](#installation)
|
||||
* [Command Line Usage](#command-line-usage)
|
||||
+ [Command line examples](#command-line-examples)
|
||||
+ [Tutorial](#tutorial)
|
||||
@@ -25,6 +25,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
+ [ExifInfo](#exifinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [ImportInfo](#importinfo)
|
||||
+ [ProjectInfo](#projectinfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
@@ -52,13 +53,12 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
## Supported operating systems
|
||||
|
||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Big Sur (10.16/11.3).
|
||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Monterey (12.0.1). Tested on both x86 and Apple silicon (M1).
|
||||
|
||||
If you have access to the macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please visit the [Discussions](https://github.com/RhetTbull/osxphotos/discussions) page and let me know!
|
||||
|
||||
| macOS Version | macOS name | Photos.app version |
|
||||
| ----------------- |------------|:-------------------|
|
||||
| 12.0 | Monterey | ?.0 UNKNOWN |
|
||||
| 12.0 | Monterey | 7.0 ✅ |
|
||||
| 10.16, 11.0-11.4 | Big Sur | 6.0 ✅ |
|
||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||
@@ -140,20 +140,22 @@ Options:
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
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.
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
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 shell
|
||||
tutorial Display osxphotos tutorial.
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
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.
|
||||
install Install Python packages into the same environment as osxphotos
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
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,...
|
||||
tutorial Display osxphotos tutorial.
|
||||
uninstall Uninstall Python packages from the osxphotos environment
|
||||
```
|
||||
|
||||
To get help on a specific command, use `osxphotos help <command_name>`
|
||||
@@ -613,7 +615,8 @@ Options:
|
||||
FILENAME. If more than one --name options is
|
||||
specified, they are treated as "OR", e.g. find
|
||||
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
|
||||
FILE. Format is a single UUID per line. Lines
|
||||
preceded with # are ignored.
|
||||
@@ -730,6 +733,15 @@ Options:
|
||||
repeating '--regex' with different arguments.
|
||||
--selected Filter for photos that are currently selected
|
||||
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
|
||||
will be evaluated in context of the following
|
||||
python list comprehension: `photos = [photo
|
||||
@@ -826,6 +838,11 @@ Options:
|
||||
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).
|
||||
--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
|
||||
original filename for export. Note: Starting
|
||||
with Photos 5, all photos are renamed upon
|
||||
@@ -1137,14 +1154,13 @@ Options:
|
||||
You can run more than one function by
|
||||
repeating the '--post-function' option with
|
||||
different arguments. See Post Function below.
|
||||
--exportdb EXPORTDB_FILE Specify alternate name for database file which
|
||||
--exportdb EXPORTDB_FILE Specify alternate path for database file which
|
||||
stores state information for export and
|
||||
--update. If --exportdb is not specified,
|
||||
export database will be saved to
|
||||
'.osxphotos_export.db' in the export
|
||||
directory. Must be specified as filename
|
||||
only, not a path, as export database will be
|
||||
saved in export directory.
|
||||
directory. If --exportdb is specified, it
|
||||
will be saved to the specified file.
|
||||
--load-config <config file path>
|
||||
Load options from file as written with --save-
|
||||
config. This allows you to save a complex
|
||||
@@ -1704,7 +1720,7 @@ Substitution Description
|
||||
{lf} A line feed: '\n', alias for {newline}
|
||||
{cr} A carriage return: '\r'
|
||||
{crlf} a carriage return + line feed: '\r\n'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.43.4'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.44.4'
|
||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified for
|
||||
@@ -1719,6 +1735,13 @@ Substitution Description
|
||||
{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
|
||||
{person} Person(s) / face(s) in a photo
|
||||
{label} Image categorization label associated with a photo
|
||||
@@ -1822,7 +1845,7 @@ COMMAND is an osxphotos template string which will be rendered and passed to the
|
||||
shell for execution. CATEGORY is the category of file to pass to COMMAND. The
|
||||
following categories are available:
|
||||
|
||||
Catgory Description
|
||||
Category Description
|
||||
exported All exported files
|
||||
new When used with '--update', all newly exported files
|
||||
updated When used with '--update', all files which were
|
||||
@@ -1863,13 +1886,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
|
||||
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
|
||||
'{filepath.name}' 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 thus renders to:
|
||||
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. If '{filepath}' is 'IMG
|
||||
1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus
|
||||
renders to:
|
||||
|
||||
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
|
||||
|
||||
@@ -2093,7 +2116,7 @@ keywords = photosdb.keywords
|
||||
|
||||
Returns a list of the keywords found in the Photos library
|
||||
|
||||
#### `album_info`
|
||||
#### <a name="photosdbalbuminfo">`album_info`</a>
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
albums = photosdb.album_info
|
||||
@@ -2123,6 +2146,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.
|
||||
|
||||
#### `project_info`
|
||||
|
||||
Returns a list of [ProjectInfo](#projectinfo) objects representing the projects/creations (cards, calendars, etc.) in the database.
|
||||
|
||||
#### `folder_info`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -2412,11 +2439,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).
|
||||
|
||||
#### `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`
|
||||
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`
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
@@ -2924,6 +2955,23 @@ Returns the start date as a timezone aware datetime.datetime object for when the
|
||||
#### `end_date`
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -3574,10 +3622,13 @@ The following template field substitutions are availabe for use the templating s
|
||||
|{lf}|A line feed: '\n', alias for {newline}|
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.43.4'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.44.4'|
|
||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||
|{album}|Album(s) photo is contained in|
|
||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||
|{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|
|
||||
|{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.|
|
||||
@@ -3679,13 +3730,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.
|
||||
|
||||
#### `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
|
||||
|
||||
@@ -3801,6 +3845,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt=""/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://spencerchang.me"><img src="https://avatars.githubusercontent.com/u/14796580?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Spencer Chang</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aspencerc99" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://www.cs.purdue.edu/homes/dgleich"><img src="https://avatars.githubusercontent.com/u/33995?v=4?s=75" width="75px;" alt=""/><br /><sub><b>David Gleich</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dgleich" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt=""/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
build
|
||||
m2r2
|
||||
pyinstaller==4.4
|
||||
pytest-mock
|
||||
pytest==6.2.4
|
||||
pytest-mock==3.6.1
|
||||
Sphinx==4.0.2
|
||||
sphinx-rtd-theme==0.5.2
|
||||
wheel==0.36.2
|
||||
twine==3.4.1
|
||||
pyinstaller==4.3
|
||||
sphinx_click
|
||||
sphinx_rtd_theme
|
||||
twine
|
||||
wheel
|
||||
Sphinx
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 7a3415c9b6b46da1269550f16ddeb35c
|
||||
config: abcd83bede460ffb3604a85d16e98db7
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overview: module code — osxphotos 0.43.4 documentation</title>
|
||||
<title>Overview: module code — osxphotos 0.43.9 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
||||
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||
@@ -93,7 +93,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.43.4 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.43.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
|
||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -313,7 +313,7 @@
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">exported_files</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">filename</span><span class="p">:</span>
|
||||
<span class="c1"># nothing got exported</span>
|
||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># need to find actual filename as sometimes Photos renames JPG to jpeg on export</span>
|
||||
<span class="c1"># may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)</span>
|
||||
@@ -1557,16 +1557,31 @@
|
||||
<span class="n">edited_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">dest_exists</span> <span class="ow">and</span> <span class="p">(</span><span class="n">update</span> <span class="ow">or</span> <span class="n">overwrite</span><span class="p">):</span>
|
||||
<span class="c1"># need to remove the destination first</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Error removing file </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> ((</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||
<span class="k">if</span> <span class="n">export_as_hardlink</span><span class="p">:</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Error hardlinking </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||
<span class="k">elif</span> <span class="n">convert_to_jpeg</span><span class="p">:</span>
|
||||
<span class="c1"># use convert_to_jpeg to export the file</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">convert_to_jpeg</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">,</span> <span class="n">compression_quality</span><span class="o">=</span><span class="n">jpeg_quality</span><span class="p">)</span>
|
||||
<span class="n">converted_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
||||
<span class="n">converted_to_jpeg_files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Error copying file </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_str</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||
|
||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
||||
<span class="n">filename</span><span class="o">=</span><span class="n">dest_str</span><span class="p">,</span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.42.87 documentation</title>
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.43.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
|
||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -68,6 +68,7 @@
|
||||
<span class="p">)</span>
|
||||
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..momentinfo</span> <span class="kn">import</span> <span class="n">MomentInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
|
||||
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
|
||||
@@ -527,6 +528,18 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">moment</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Moment photo belongs to"""</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_moment</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="n">MomentInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">,</span> <span class="n">moment_pk</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"momentID"</span><span class="p">])</span>
|
||||
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_moment</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""list of albums picture is contained in"""</span>
|
||||
@@ -876,7 +889,7 @@
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||
<span class="n">live_model_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"live_model_id"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">live_model_id</span> <span class="o">==</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">live_model_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
@@ -897,28 +910,20 @@
|
||||
<span class="c1"># photos 4 has "isOnDisk" column we could check</span>
|
||||
<span class="c1"># or could do the actual check with "isfile"</span>
|
||||
<span class="c1"># TODO: should this be a warning or debug?</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># Photos 5</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># In testing, I've seen occasional missing movie for live photo</span>
|
||||
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
|
||||
<span class="c1"># TODO: should this be a warning or debug?</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># In testing, I've seen occasional missing movie for live photo</span>
|
||||
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
|
||||
<span class="c1"># TODO: should this be a warning or debug?</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.42.92 documentation</title>
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.43.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
|
||||
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -45,12 +45,14 @@
|
||||
<span class="kn">import</span> <span class="nn">sys</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.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">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">import</span> <span class="nn">bitmath</span>
|
||||
<span class="kn">import</span> <span class="nn">photoscript</span>
|
||||
<span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
|
||||
@@ -283,6 +285,10 @@
|
||||
<span class="c1"># Dict to hold information on volume names (Photos 5+)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_filesystem_volumes</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="c1"># Dict to hold information on moments (Photos 5+)</span>
|
||||
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
@@ -2524,6 +2530,10 @@
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing comments and likes for shared photos."</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_comments</span><span class="p">()</span>
|
||||
|
||||
<span class="c1"># process moments</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing moments."</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_moments</span><span class="p">()</span>
|
||||
|
||||
<span class="c1"># done processing, dump debug data if requested</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Done processing details from Photos library."</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
||||
@@ -2569,6 +2579,109 @@
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">"Burst Photos (dbphotos_burst:"</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">))</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_process_moments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Process data from ZMOMENT table"""</span>
|
||||
<span class="c1"># _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Moment info implemented for this database version"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_moment_5</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_process_moment_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Process moment info for Photos 5 databases"""</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"""</span>
|
||||
<span class="s2"> SELECT </span>
|
||||
<span class="s2"> Z_PK,</span>
|
||||
<span class="s2"> ZTIMEZONEOFFSET,</span>
|
||||
<span class="s2"> ZTRASHEDSTATE,</span>
|
||||
<span class="s2"> ZAPPROXIMATELATITUDE,</span>
|
||||
<span class="s2"> ZAPPROXIMATELONGITUDE,</span>
|
||||
<span class="s2"> ZENDDATE,</span>
|
||||
<span class="s2"> ZMODIFICATIONDATE,</span>
|
||||
<span class="s2"> ZREPRESENTATIVEDATE,</span>
|
||||
<span class="s2"> ZSTARTDATE,</span>
|
||||
<span class="s2"> ZSUBTITLE,</span>
|
||||
<span class="s2"> ZTITLE,</span>
|
||||
<span class="s2"> ZUUID</span>
|
||||
<span class="s2"> FROM ZMOMENT"""</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># results</span>
|
||||
<span class="c1"># 0 Z_PK,</span>
|
||||
<span class="c1"># 1 ZTIMEZONEOFFSET,</span>
|
||||
<span class="c1"># 2 ZTRASHEDSTATE,</span>
|
||||
<span class="c1"># 3 ZAPPROXIMATELATITUDE,</span>
|
||||
<span class="c1"># 4 ZAPPROXIMATELONGITUDE,</span>
|
||||
<span class="c1"># 5 ZENDDATE,</span>
|
||||
<span class="c1"># 6 ZMODIFICATIONDATE,</span>
|
||||
<span class="c1"># 7 ZREPRESENTATIVEDATE,</span>
|
||||
<span class="c1"># 8 ZSTARTDATE,</span>
|
||||
<span class="c1"># 9 ZSUBTITLE,</span>
|
||||
<span class="c1"># 10 ZTITLE,</span>
|
||||
<span class="c1"># 11 ZUUID</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
|
||||
<span class="n">moment_info</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"pk"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"timezoneOffset"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"trashedState"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLatitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLongitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"endDate"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"modificationDate"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"representativeDate"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"startDate"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"subtitle"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"title"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># if both lat/lon == -180, then it means location undefined</span>
|
||||
<span class="k">if</span> <span class="p">(</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLatitude"</span><span class="p">]</span> <span class="o">==</span> <span class="o">-</span><span class="mf">180.0</span>
|
||||
<span class="ow">and</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLongitude"</span><span class="p">]</span> <span class="o">==</span> <span class="o">-</span><span class="mf">180.0</span>
|
||||
<span class="p">):</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"latitude"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"longitude"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"latitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLatitude"</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"longitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLongitude"</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># process date stamps</span>
|
||||
<span class="n">offset_seconds</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"timezoneOffset"</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span>
|
||||
<span class="n">delta</span> <span class="o">=</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">seconds</span><span class="o">=</span><span class="n">offset_seconds</span><span class="p">)</span>
|
||||
<span class="n">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">date_name</span> <span class="ow">in</span> <span class="p">[</span>
|
||||
<span class="s2">"startDate"</span><span class="p">,</span>
|
||||
<span class="s2">"endDate"</span><span class="p">,</span>
|
||||
<span class="s2">"modificationDate"</span><span class="p">,</span>
|
||||
<span class="s2">"representativeDate"</span><span class="p">,</span>
|
||||
<span class="p">]:</span>
|
||||
<span class="n">date_stamp</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">moment_date</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="n">date_stamp</span> <span class="o">+</span> <span class="n">TIME_DELTA</span><span class="p">)</span>
|
||||
<span class="c1"># save raw time stamp valu</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span> <span class="o">+</span> <span class="s2">"_timestamp"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
|
||||
<span class="c1"># sometimes imageDate is invalid so use 1 Jan 1970 in UTC as image date</span>
|
||||
<span class="n">moment_date</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">(</span><span class="mi">1970</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
<span class="n">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">timedelta</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span> <span class="o">+</span> <span class="s2">"_timestamp"</span><span class="p">]</span> <span class="o">=</span> <span class="n">date_stamp</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># process title/subtitle</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"title"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"title"</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">""</span>
|
||||
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"subtitle"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"subtitle"</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">""</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span><span class="p">[</span><span class="n">moment_info</span><span class="p">[</span><span class="s2">"pk"</span><span class="p">]]</span> <span class="o">=</span> <span class="n">moment_info</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_build_album_folder_hierarchy_5</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">,</span> <span class="n">folders</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""recursively build folder/album hierarchy</span>
|
||||
<span class="sd"> uuid: uuid of the album/folder being processed</span>
|
||||
@@ -3391,6 +3504,34 @@
|
||||
<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="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">""</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">""</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">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>
|
||||
@@ -3399,6 +3540,7 @@
|
||||
|
||||
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span>
|
||||
<span class="sd">"""Execute sql statement and return cursor"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_duplicate_signature</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
|
||||
@@ -3517,7 +3659,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
3
docs/_static/basic.css
vendored
@@ -731,8 +731,9 @@ dl.glossary dt {
|
||||
|
||||
.classifier:before {
|
||||
font-style: normal;
|
||||
margin: 0.5em;
|
||||
margin: 0 0.5em;
|
||||
content: ":";
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
abbr, acronym {
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.43.4',
|
||||
VERSION: '0.44.4',
|
||||
LANGUAGE: 'None',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
5
docs/_static/searchtools.js
vendored
@@ -328,7 +328,9 @@ var Search = {
|
||||
var results = [];
|
||||
|
||||
for (var prefix in objects) {
|
||||
for (var name in objects[prefix]) {
|
||||
for (var iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
|
||||
var match = objects[prefix][iMatch];
|
||||
var name = match[4];
|
||||
var fullname = (prefix ? prefix + '.' : '') + name;
|
||||
var fullnameLower = fullname.toLowerCase()
|
||||
if (fullnameLower.indexOf(object) > -1) {
|
||||
@@ -342,7 +344,6 @@ var Search = {
|
||||
} else if (parts[parts.length - 1].indexOf(object) > -1) {
|
||||
score += Scorer.objPartialMatch;
|
||||
}
|
||||
var match = objects[prefix][name];
|
||||
var objname = objnames[match[1]][2];
|
||||
var title = titles[match[0]];
|
||||
// If more than one term searched for, we require other words to be
|
||||
|
||||
1592
docs/cli.html
2342
docs/genindex.html
104
docs/index.html
@@ -4,8 +4,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.43.4 documentation</title>
|
||||
<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 osxphotos’s documentation! — osxphotos 0.44.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
@@ -31,30 +32,30 @@
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="welcome-to-osxphotos-s-documentation">
|
||||
<section id="welcome-to-osxphotos-s-documentation">
|
||||
<h1>Welcome to osxphotos’s documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline">¶</a></h1>
|
||||
</div>
|
||||
<div class="section" id="osxphotos">
|
||||
</section>
|
||||
<section id="osxphotos">
|
||||
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="section" id="what-is-osxphotos">
|
||||
<section id="what-is-osxphotos">
|
||||
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline">¶</a></h2>
|
||||
<p>OSXPhotos provides both the ability to interact with and query Apple’s Photos.app library on macOS directly from your python code
|
||||
as well as a very flexible command line interface (CLI) app for exporting photos.
|
||||
You can query the Photos library database – for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
|
||||
You can also easily export both the original and edited photos.</p>
|
||||
</div>
|
||||
<div class="section" id="supported-operating-systems">
|
||||
</section>
|
||||
<section id="supported-operating-systems">
|
||||
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).</p>
|
||||
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
|
||||
<p>This package will read Photos databases for any supported version on any supported macOS version.
|
||||
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
|
||||
<p>Requires python >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
</section>
|
||||
<section id="installation">
|
||||
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline">¶</a></h2>
|
||||
<p>If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.</p>
|
||||
<div class="section" id="installation-using-pipx">
|
||||
<section id="installation-using-pipx">
|
||||
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline">¶</a></h3>
|
||||
<p>If you aren’t familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
|
||||
<ul class="simple">
|
||||
@@ -64,15 +65,15 @@ E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine
|
||||
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
|
||||
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation-using-pip">
|
||||
</section>
|
||||
<section id="installation-using-pip">
|
||||
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline">¶</a></h3>
|
||||
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="installation-from-git-repository">
|
||||
</section>
|
||||
<section id="installation-from-git-repository">
|
||||
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline">¶</a></h3>
|
||||
<p>OSXPhotos uses setuptools, thus simply run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
|
||||
@@ -87,9 +88,9 @@ I recommend you install the latest version from <a class="reference external" hr
|
||||
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
|
||||
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
|
||||
If you aren’t comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="command-line-usage">
|
||||
</section>
|
||||
</section>
|
||||
<section id="command-line-usage">
|
||||
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h2>
|
||||
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
|
||||
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
|
||||
@@ -127,38 +128,38 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre"><command_name></span></code></p>
|
||||
<div class="section" id="command-line-examples">
|
||||
<section id="command-line-examples">
|
||||
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline">¶</a></h3>
|
||||
<div class="section" id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
|
||||
<section id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
|
||||
<h4>export all photos to ~/Desktop/export group in folders by date created<a class="headerlink" href="#export-all-photos-to-desktop-export-group-in-folders-by-date-created" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
<p><strong>Note</strong>: Photos library/database path can also be specified using <code class="docutils literal notranslate"><span class="pre">--db</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">--db</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
</section>
|
||||
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">>results.json</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
<h4>export photos to file structure based on 4-digit year and full name of month of photo’s creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{created.month}"</span></code></p>
|
||||
<p>(by default, it will attempt to use the system library)</p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
<h4>export photos to file structure based on 4-digit year of photo’s creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{media_type}"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
</section>
|
||||
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
<h4>export default library using ‘country name/year’ as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
</section>
|
||||
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">"Big</span> <span class="pre">Videos"</span></code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="example-uses-of-the-package">
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section id="example-uses-of-the-package">
|
||||
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Simple usage of the package """</span>
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
@@ -274,48 +275,29 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="package-interface">
|
||||
</section>
|
||||
<section id="package-interface">
|
||||
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
|
||||
<div class="toctree-wrapper compound">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-persons">persons</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-repl">repl</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="indices-and-tables">
|
||||
</section>
|
||||
</section>
|
||||
<section id="indices-and-tables">
|
||||
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -373,7 +355,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
©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.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos — osxphotos 0.43.4 documentation</title>
|
||||
<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 — osxphotos 0.44.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
@@ -30,11 +31,11 @@
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="osxphotos">
|
||||
<section id="osxphotos">
|
||||
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="toctree-wrapper compound">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -91,7 +92,7 @@
|
||||
©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.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
|
||||
BIN
docs/objects.inv
1349
docs/reference.html
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.43.4 documentation</title>
|
||||
<title>Search — osxphotos 0.44.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
©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.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from ._constants import AlbumSortOrder
|
||||
from ._version import __version__
|
||||
from .exiftool import ExifTool
|
||||
from .photoinfo import ExportResults, PhotoInfo
|
||||
from .photoexporter import ExportResults, PhotoExporter
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photosdb import PhotosDB
|
||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||
from .phototemplate import PhotoTemplate
|
||||
|
||||
@@ -30,6 +30,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
||||
# Photos 6 (10.16.0 Beta) == 14104
|
||||
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
|
||||
|
||||
_PHOTOS_2_VERSION = "2622"
|
||||
|
||||
# only version 3 - 4 have RKVersion.selfPortrait
|
||||
_PHOTOS_3_VERSION = "3301"
|
||||
|
||||
@@ -121,12 +123,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||
# Constants used for processing folders and albums
|
||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user 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_ROOT_FOLDER_KIND = 3999 # root folder
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
||||
|
||||
_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"
|
||||
|
||||
# EXIF related constants
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.43.4"
|
||||
__version__ = "0.44.4"
|
||||
|
||||
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from ._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
@@ -161,7 +161,6 @@ class AlbumInfoBaseClass:
|
||||
|
||||
class AlbumInfo(AlbumInfoBaseClass):
|
||||
"""
|
||||
Base class for AlbumInfo, ImportInfo
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
@@ -231,7 +230,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
||||
self._parent = (
|
||||
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:
|
||||
@@ -266,18 +265,17 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
|
||||
def photo_index(self, photo):
|
||||
"""return index of photo in album (based on album sort order)"""
|
||||
index = 0
|
||||
for p in self.photos:
|
||||
for index, p in enumerate(self.photos):
|
||||
if p.uuid == photo.uuid:
|
||||
return index
|
||||
index += 1
|
||||
else:
|
||||
raise ValueError(
|
||||
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):
|
||||
"""Information about import sessions"""
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
"""return list of photos contained in import session"""
|
||||
@@ -296,6 +294,15 @@ class ImportInfo(AlbumInfoBaseClass):
|
||||
return self._photos
|
||||
|
||||
|
||||
class ProjectInfo(AlbumInfo):
|
||||
"""
|
||||
ProjectInfo with info about projects
|
||||
Projects are cards, calendars, slideshows, etc.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class FolderInfo:
|
||||
"""
|
||||
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"]
|
||||
self._parent = (
|
||||
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:
|
||||
|
||||
135
osxphotos/cli.py
@@ -12,6 +12,7 @@ import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from runpy import run_module
|
||||
|
||||
import bitmath
|
||||
import click
|
||||
@@ -55,14 +56,15 @@ from .exiftool import get_exiftool_path
|
||||
from .export_db import ExportDB, ExportDBInMemory
|
||||
from .fileutil import FileUtil, FileUtilNoOp
|
||||
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||
from .photoinfo import ExportResults, PhotoInfo
|
||||
from .photoexporter import ExportResults, PhotoExporter
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photokit import check_photokit_authorization, request_photokit_authorization
|
||||
from .photosalbum import PhotosAlbum
|
||||
from .phototemplate import PhotoTemplate, RenderOptions
|
||||
from .pyrepl import embed_repl
|
||||
from .queryoptions import QueryOptions
|
||||
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
|
||||
# set via --verbose/-V
|
||||
@@ -297,7 +299,8 @@ def QUERY_OPTIONS(f):
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s).",
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
o(
|
||||
"--uuid-from-file",
|
||||
@@ -542,6 +545,17 @@ def QUERY_OPTIONS(f):
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
o(
|
||||
"--exif",
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
o(
|
||||
"--query-eval",
|
||||
metavar="CRITERIA",
|
||||
@@ -687,6 +701,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 "
|
||||
"(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(
|
||||
"--current-name",
|
||||
is_flag=True,
|
||||
@@ -1029,10 +1060,9 @@ def cli(ctx, db, json_, debug):
|
||||
metavar="EXPORTDB_FILE",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify alternate name for database file which stores state information for export and --update. "
|
||||
"Specify alternate path for database file which stores state information for export and --update. "
|
||||
f"If --exportdb is not specified, export database will be saved to '{OSXPHOTOS_EXPORT_DB}' "
|
||||
"in the export directory. Must be specified as filename only, not a path, as export database "
|
||||
"will be saved in export directory."
|
||||
"in the export directory. If --exportdb is specified, it will be saved to the specified file. "
|
||||
),
|
||||
type=click.Path(),
|
||||
)
|
||||
@@ -1112,6 +1142,8 @@ def export(
|
||||
skip_bursts,
|
||||
skip_live,
|
||||
skip_raw,
|
||||
skip_uuid,
|
||||
skip_uuid_from_file,
|
||||
person_keyword,
|
||||
album_keyword,
|
||||
keyword_template,
|
||||
@@ -1189,6 +1221,7 @@ def export(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
query_eval,
|
||||
query_function,
|
||||
duplicate,
|
||||
@@ -1279,6 +1312,8 @@ def export(
|
||||
skip_bursts = cfg.skip_bursts
|
||||
skip_live = cfg.skip_live
|
||||
skip_raw = cfg.skip_raw
|
||||
skip_uuid = cfg.skip_uuid
|
||||
skip_uuid_from_file = cfg.skip_uuid_from_file
|
||||
person_keyword = cfg.person_keyword
|
||||
album_keyword = cfg.album_keyword
|
||||
keyword_template = cfg.keyword_template
|
||||
@@ -1353,6 +1388,7 @@ def export(
|
||||
max_size = cfg.max_size
|
||||
regex = cfg.regex
|
||||
selected = cfg.selected
|
||||
exif = cfg.exif
|
||||
query_eval = cfg.query_eval
|
||||
query_function = cfg.query_function
|
||||
duplicate = cfg.duplicate
|
||||
@@ -1536,25 +1572,23 @@ def export(
|
||||
|
||||
# sanity check exportdb
|
||||
if exportdb and exportdb != OSXPHOTOS_EXPORT_DB:
|
||||
if "/" in exportdb:
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Error: --exportdb must be specified as filename not path; "
|
||||
+ f"export database will saved in export directory '{dest}'.",
|
||||
fg=CLI_COLOR_ERROR,
|
||||
)
|
||||
)
|
||||
raise click.Abort()
|
||||
elif pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists():
|
||||
if pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists():
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Warning: export database is '{exportdb}' but found '{OSXPHOTOS_EXPORT_DB}' in {dest}; using '{exportdb}'",
|
||||
fg=CLI_COLOR_WARNING,
|
||||
)
|
||||
)
|
||||
if pathlib.Path(exportdb).resolve().parent != pathlib.Path(dest):
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Warning: export database '{pathlib.Path(exportdb).resolve()}' is in a different directory than export destination '{dest}'",
|
||||
fg=CLI_COLOR_WARNING,
|
||||
)
|
||||
)
|
||||
|
||||
# open export database and assign copy/link/unlink functions
|
||||
export_db_path = os.path.join(dest, exportdb or OSXPHOTOS_EXPORT_DB)
|
||||
export_db_path = exportdb or os.path.join(dest, OSXPHOTOS_EXPORT_DB)
|
||||
|
||||
# check that export isn't in the parent or child of a previously exported library
|
||||
other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB)
|
||||
@@ -1577,10 +1611,10 @@ def export(
|
||||
click.confirm("Do you want to continue?", abort=True)
|
||||
|
||||
if dry_run:
|
||||
export_db = ExportDBInMemory(export_db_path)
|
||||
export_db = ExportDBInMemory(dbfile=export_db_path, export_dir=dest)
|
||||
fileutil = FileUtilNoOp
|
||||
else:
|
||||
export_db = ExportDB(export_db_path)
|
||||
export_db = ExportDB(dbfile=export_db_path, export_dir=dest)
|
||||
fileutil = FileUtil
|
||||
|
||||
if verbose_:
|
||||
@@ -1672,6 +1706,7 @@ def export(
|
||||
max_size=max_size,
|
||||
regex=regex,
|
||||
selected=selected,
|
||||
exif=exif,
|
||||
query_eval=query_eval,
|
||||
function=query_function,
|
||||
duplicate=duplicate,
|
||||
@@ -1688,6 +1723,13 @@ def export(
|
||||
else:
|
||||
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:
|
||||
# ignore previously exported files
|
||||
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
|
||||
@@ -2084,6 +2126,7 @@ def query(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
query_eval,
|
||||
query_function,
|
||||
add_to_album,
|
||||
@@ -2119,6 +2162,7 @@ def query(
|
||||
max_size,
|
||||
regex,
|
||||
selected,
|
||||
exif,
|
||||
duplicate,
|
||||
]
|
||||
exclusive = [
|
||||
@@ -2250,6 +2294,7 @@ def query(
|
||||
function=query_function,
|
||||
regex=regex,
|
||||
selected=selected,
|
||||
exif=exif,
|
||||
duplicate=duplicate,
|
||||
)
|
||||
|
||||
@@ -2907,7 +2952,8 @@ def export_photo_to_directory(
|
||||
tries += 1
|
||||
error = 0
|
||||
try:
|
||||
export_results = photo.export2(
|
||||
exporter = PhotoExporter(photo)
|
||||
export_results = exporter.export2(
|
||||
dest_path,
|
||||
original_filename=filename,
|
||||
edited=edited,
|
||||
@@ -2969,7 +3015,7 @@ def export_photo_to_directory(
|
||||
break
|
||||
else:
|
||||
click.echo(
|
||||
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(
|
||||
@@ -3358,11 +3404,13 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
||||
Returns:
|
||||
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 = []
|
||||
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:
|
||||
verbose_(f"Deleting {p}")
|
||||
fileutil.unlink(p)
|
||||
@@ -3417,7 +3465,7 @@ def write_finder_tags(
|
||||
skipped = []
|
||||
if keywords:
|
||||
# match whatever keywords would've been used in --exiftool or --sidecar
|
||||
exif = photo._exiftool_dict(
|
||||
exif = PhotoExporter(photo)._exiftool_dict(
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
@@ -3602,6 +3650,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)
|
||||
@DB_OPTION
|
||||
@DB_ARGUMENT
|
||||
@@ -3615,7 +3687,8 @@ def run_post_command(
|
||||
@click.option(
|
||||
"--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,
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@@ -4123,6 +4196,7 @@ def _spotlight_photo(photo: PhotoInfo):
|
||||
)
|
||||
def repl(ctx, cli_obj, db, emacs):
|
||||
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
|
||||
import logging
|
||||
|
||||
from objexplore import explore
|
||||
from photoscript import Album, Photo, PhotosLibrary
|
||||
@@ -4130,8 +4204,15 @@ def repl(ctx, cli_obj, db, emacs):
|
||||
|
||||
from osxphotos import ExifTool, PhotoInfo, PhotosDB
|
||||
from osxphotos.albuminfo import AlbumInfo
|
||||
from osxphotos.momentinfo import MomentInfo
|
||||
from osxphotos.photoexporter import ExportResults, PhotoExporter
|
||||
from osxphotos.placeinfo import PlaceInfo
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.scoreinfo import ScoreInfo
|
||||
from osxphotos.searchinfo import SearchInfo
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.disabled = True
|
||||
|
||||
pretty.install()
|
||||
print(f"python version: {sys.version}")
|
||||
@@ -4161,7 +4242,9 @@ def repl(ctx, cli_obj, db, emacs):
|
||||
|
||||
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
|
||||
print("The following classes have been imported from osxphotos:")
|
||||
print("- AlbumInfo, ExifTool, PhotoInfo, PhotosDB, PlaceInfo, QueryOptions\n")
|
||||
print(
|
||||
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
|
||||
)
|
||||
print("The following variables are defined:")
|
||||
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
|
||||
print(
|
||||
|
||||
@@ -207,7 +207,7 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "The following categories are available: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Catgory", "Description")]
|
||||
templ_tuples = [("Category", "Description")]
|
||||
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
formatter.write("\n")
|
||||
@@ -224,13 +224,13 @@ The following attributes may be used with '--xattr-template':
|
||||
)
|
||||
formatter.write("\n")
|
||||
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_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. "
|
||||
"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: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
|
||||
28
osxphotos/exifinfo.py
Normal file
@@ -0,0 +1,28 @@
|
||||
""" ExifInfo class to expose EXIF info from the library """
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExifInfo:
|
||||
"""EXIF info associated with a photo from the Photos library"""
|
||||
|
||||
flash_fired: bool
|
||||
iso: int
|
||||
metering_mode: int
|
||||
sample_rate: int
|
||||
track_format: int
|
||||
white_balance: int
|
||||
aperture: float
|
||||
bit_rate: float
|
||||
duration: float
|
||||
exposure_bias: float
|
||||
focal_length: float
|
||||
fps: float
|
||||
latitude: float
|
||||
longitude: float
|
||||
shutter_speed: float
|
||||
camera_make: str
|
||||
camera_model: str
|
||||
codec: str
|
||||
lens_model: str
|
||||
@@ -1,5 +1,6 @@
|
||||
""" Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
|
||||
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
@@ -14,7 +15,7 @@ from ._constants import OSXPHOTOS_EXPORT_DB
|
||||
from ._version import __version__
|
||||
|
||||
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
|
||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
|
||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||
|
||||
|
||||
class ExportDB_ABC(ABC):
|
||||
@@ -171,7 +172,7 @@ class ExportDBNoOp(ExportDB_ABC):
|
||||
return []
|
||||
|
||||
def get_detected_text_for_uuid(self, uuid):
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_detected_text_for_uuid(self, uuid, json_text):
|
||||
pass
|
||||
@@ -193,15 +194,14 @@ class ExportDBNoOp(ExportDB_ABC):
|
||||
class ExportDB(ExportDB_ABC):
|
||||
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
|
||||
|
||||
def __init__(self, dbfile):
|
||||
def __init__(self, dbfile, export_dir):
|
||||
"""dbfile: path to osxphotos export database file"""
|
||||
self._dbfile = dbfile
|
||||
# _path is parent of the database
|
||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this parent _path
|
||||
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this path
|
||||
# this allows the entire export tree to be moved to a new disk/location
|
||||
# whilst preserving the UUID to filename mapping
|
||||
self._path = pathlib.Path(dbfile).parent
|
||||
self._path = export_dir
|
||||
self._conn = self._open_export_db(dbfile)
|
||||
self._insert_run_info()
|
||||
|
||||
@@ -214,14 +214,13 @@ class ExportDB(ExportDB_ABC):
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
f"SELECT uuid FROM files WHERE filepath_normalized = ?", (filename,)
|
||||
"SELECT uuid FROM files WHERE filepath_normalized = ?", (filename,)
|
||||
)
|
||||
results = c.fetchone()
|
||||
uuid = results[0] if results else None
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
uuid = None
|
||||
|
||||
return uuid
|
||||
|
||||
def set_uuid_for_file(self, filename, uuid):
|
||||
@@ -232,9 +231,10 @@ class ExportDB(ExportDB_ABC):
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
||||
"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
||||
(filename, filename_normalized, uuid),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
@@ -274,15 +274,14 @@ class ExportDB(ExportDB_ABC):
|
||||
)
|
||||
results = c.fetchone()
|
||||
if results:
|
||||
stats = results[0:3]
|
||||
stats = results[:3]
|
||||
mtime = int(stats[2]) if stats[2] is not None else None
|
||||
stats = (stats[0], stats[1], mtime)
|
||||
else:
|
||||
stats = (None, None, None)
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
stats = (None, None, None)
|
||||
|
||||
stats = None, None, None
|
||||
return stats
|
||||
|
||||
def set_stat_edited_for_file(self, filename, stats):
|
||||
@@ -332,15 +331,14 @@ class ExportDB(ExportDB_ABC):
|
||||
)
|
||||
results = c.fetchone()
|
||||
if results:
|
||||
stats = results[0:3]
|
||||
stats = results[:3]
|
||||
mtime = int(stats[2]) if stats[2] is not None else None
|
||||
stats = (stats[0], stats[1], mtime)
|
||||
else:
|
||||
stats = (None, None, None)
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
stats = (None, None, None)
|
||||
|
||||
stats = None, None, None
|
||||
return stats
|
||||
|
||||
def set_stat_converted_for_file(self, filename, stats):
|
||||
@@ -493,7 +491,10 @@ class ExportDB(ExportDB_ABC):
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
|
||||
(uuid, text_json,),
|
||||
(
|
||||
uuid,
|
||||
text_json,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
@@ -517,9 +518,10 @@ class ExportDB(ExportDB_ABC):
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
||||
"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
||||
(filename, filename_normalized, uuid),
|
||||
)
|
||||
|
||||
c.execute(
|
||||
"UPDATE files "
|
||||
+ "SET orig_mode = ?, orig_size = ?, orig_mtime = ? "
|
||||
@@ -582,7 +584,7 @@ class ExportDB(ExportDB_ABC):
|
||||
)
|
||||
results = c.fetchone()
|
||||
if results:
|
||||
stats = results[0:3]
|
||||
stats = results[:3]
|
||||
mtime = int(stats[2]) if stats[2] is not None else None
|
||||
stats = (stats[0], stats[1], mtime)
|
||||
else:
|
||||
@@ -741,9 +743,10 @@ class ExportDB(ExportDB_ABC):
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
f"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
|
||||
"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
|
||||
(dt, python_path, cmd, args, cwd),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
@@ -755,14 +758,13 @@ class ExportDBInMemory(ExportDB):
|
||||
modifying the on-disk version
|
||||
"""
|
||||
|
||||
def __init__(self, dbfile):
|
||||
def __init__(self, dbfile, export_dir):
|
||||
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
|
||||
# _path is parent of the database
|
||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this parent _path
|
||||
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this path
|
||||
# this allows the entire export tree to be moved to a new disk/location
|
||||
# whilst preserving the UUID to filename mapping
|
||||
self._path = pathlib.Path(self._dbfile).parent
|
||||
self._path = export_dir
|
||||
self._conn = self._open_export_db(self._dbfile)
|
||||
self._insert_run_info()
|
||||
|
||||
|
||||
@@ -17,25 +17,25 @@ from wurlitzer import pipes
|
||||
|
||||
|
||||
class ImageConversionError(Exception):
|
||||
"""Base class for exceptions in this module. """
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
""" Convert images to jpeg. This class is a singleton
|
||||
which will re-use the Core Image CIContext to avoid
|
||||
creating a new context for every conversion. """
|
||||
"""Convert images to jpeg. This class is a singleton
|
||||
which will re-use the Core Image CIContext to avoid
|
||||
creating a new context for every conversion."""
|
||||
|
||||
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:
|
||||
cls.instance = super().__new__(cls)
|
||||
|
||||
return cls.instance
|
||||
|
||||
def __init__(self):
|
||||
""" return existing singleton or create a new one """
|
||||
"""return existing singleton or create a new one"""
|
||||
|
||||
if hasattr(self, "context"):
|
||||
return
|
||||
@@ -47,13 +47,10 @@ class ImageConverter:
|
||||
"workingFormat": Quartz.kCIFormatRGBAh,
|
||||
}
|
||||
)
|
||||
mtldevice = Metal.MTLCreateSystemDefaultDevice()
|
||||
self.context = Quartz.CIContext.contextWithMTLDevice_options_(
|
||||
mtldevice, context_options
|
||||
)
|
||||
self.context = Quartz.CIContext.contextWithOptions_(context_options)
|
||||
|
||||
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:
|
||||
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:
|
||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
output_colorspace = (
|
||||
input_image.colorSpace()
|
||||
or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
)
|
||||
)
|
||||
|
||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||
@@ -123,4 +123,3 @@ class ImageConverter:
|
||||
raise ImageConversionError(
|
||||
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
)
|
||||
|
||||
|
||||
69
osxphotos/momentinfo.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""MomentInfo class with details about photo moments."""
|
||||
|
||||
|
||||
class MomentInfo:
|
||||
"""Info about a photo moment"""
|
||||
|
||||
def __init__(self, db, moment_pk):
|
||||
"""Initialize with a moment PK; returns None if PK not found."""
|
||||
self._db = db
|
||||
self._pk = moment_pk
|
||||
|
||||
self._moment = self._db._db_moment_pk.get(moment_pk)
|
||||
if not self._moment:
|
||||
raise ValueError(f"No moment with PK {moment_pk}")
|
||||
|
||||
@property
|
||||
def pk(self):
|
||||
"""Primary key of the moment."""
|
||||
return self._pk
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
"""Location of the moment."""
|
||||
return (self._moment.get("latitude"), self._moment.get("longitude"))
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Title of the moment."""
|
||||
return self._moment.get("title")
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
"""Subtitle of the moment."""
|
||||
return self._moment.get("subtitle")
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
"""Start date of the moment."""
|
||||
return self._moment.get("startDate")
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
"""Stop date of the moment."""
|
||||
return self._moment.get("endDate")
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""Date of the moment."""
|
||||
return self._moment.get("representativeDate")
|
||||
|
||||
@property
|
||||
def modification_date(self):
|
||||
"""Modification date of the moment."""
|
||||
return self._moment.get("modificationDate")
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
"""All photos in this moment"""
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
photo_uuids = [
|
||||
uuid
|
||||
for uuid, photo in self._db._dbphotos.items()
|
||||
if photo["momentID"] == self._pk
|
||||
]
|
||||
|
||||
self._photos = self._db.photos_by_uuid(photo_uuids)
|
||||
return self._photos
|
||||
2171
osxphotos/photoexporter.py
Normal file
@@ -16,14 +16,18 @@ from typing import Optional
|
||||
import yaml
|
||||
from osxmetadata import OSXMetaData
|
||||
|
||||
from .._constants import (
|
||||
from ._constants import (
|
||||
_MOVIE_TYPE,
|
||||
_PHOTO_TYPE,
|
||||
_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_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
@@ -33,15 +37,21 @@ from .._constants import (
|
||||
BURST_SELECTED,
|
||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
||||
)
|
||||
from ..adjustmentsinfo import AdjustmentsInfo
|
||||
from ..albuminfo import AlbumInfo, ImportInfo
|
||||
from ..personinfo import FaceInfo, PersonInfo
|
||||
from ..phototemplate import PhotoTemplate, RenderOptions
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..query_builder import get_query
|
||||
from ..text_detection import detect_text
|
||||
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
||||
from ..utils import _debug, _get_resource_loc, findfiles
|
||||
from .adjustmentsinfo import AdjustmentsInfo
|
||||
from .albuminfo import AlbumInfo, ImportInfo, ProjectInfo
|
||||
from .exifinfo import ExifInfo
|
||||
from .exiftool import ExifToolCaching, get_exiftool_path
|
||||
from .momentinfo import MomentInfo
|
||||
from .personinfo import FaceInfo, PersonInfo
|
||||
from .photoexporter import PhotoExporter
|
||||
from .phototemplate import PhotoTemplate, RenderOptions
|
||||
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from .query_builder import get_query
|
||||
from .scoreinfo import ScoreInfo
|
||||
from .searchinfo import SearchInfo
|
||||
from .text_detection import detect_text
|
||||
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
||||
from .utils import _debug, _get_resource_loc, findfiles
|
||||
|
||||
|
||||
class PhotoInfo:
|
||||
@@ -50,42 +60,12 @@ class PhotoInfo:
|
||||
including keywords, persons, albums, uuid, path, etc.
|
||||
"""
|
||||
|
||||
# import additional methods
|
||||
from ._photoinfo_comments import comments, likes
|
||||
from ._photoinfo_exifinfo import ExifInfo, exif_info
|
||||
from ._photoinfo_exiftool import exiftool
|
||||
from ._photoinfo_export import (
|
||||
ExportResults,
|
||||
_exiftool_dict,
|
||||
_exiftool_json_sidecar,
|
||||
_export_photo,
|
||||
_export_photo_with_photos_export,
|
||||
_get_exif_keywords,
|
||||
_get_exif_persons,
|
||||
_write_exif_data,
|
||||
_write_sidecar,
|
||||
_xmp_sidecar,
|
||||
export,
|
||||
export2,
|
||||
)
|
||||
from ._photoinfo_scoreinfo import ScoreInfo, score
|
||||
from ._photoinfo_searchinfo import (
|
||||
SearchInfo,
|
||||
labels,
|
||||
labels_normalized,
|
||||
search_info,
|
||||
search_info_normalized,
|
||||
)
|
||||
|
||||
def __init__(self, db=None, uuid=None, info=None):
|
||||
self._uuid = uuid
|
||||
self._info = info
|
||||
self._db = db
|
||||
self._verbose = self._db._verbose
|
||||
|
||||
# TODO: remove this once refactor of PhotoExporter is done
|
||||
self._render_options = RenderOptions()
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""filename of the picture"""
|
||||
@@ -494,6 +474,18 @@ class PhotoInfo:
|
||||
self._faceinfo = []
|
||||
return self._faceinfo
|
||||
|
||||
@property
|
||||
def moment(self):
|
||||
"""Moment photo belongs to"""
|
||||
try:
|
||||
return self._moment
|
||||
except AttributeError:
|
||||
try:
|
||||
self._moment = MomentInfo(db=self._db, moment_pk=self._info["momentID"])
|
||||
except ValueError:
|
||||
self._moment = None
|
||||
return self._moment
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
"""list of albums picture is contained in"""
|
||||
@@ -557,6 +549,18 @@ class PhotoInfo:
|
||||
)
|
||||
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
|
||||
def keywords(self):
|
||||
"""list of keywords for picture"""
|
||||
@@ -843,7 +847,7 @@ class PhotoInfo:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.live_photo and not self.ismissing:
|
||||
live_model_id = self._info["live_model_id"]
|
||||
if live_model_id == None:
|
||||
if live_model_id is None:
|
||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||
photopath = None
|
||||
else:
|
||||
@@ -864,28 +868,20 @@ class PhotoInfo:
|
||||
# photos 4 has "isOnDisk" column we could check
|
||||
# or could do the actual check with "isfile"
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
else:
|
||||
# Photos 5
|
||||
if self.live_photo and not self.ismissing:
|
||||
filename = pathlib.Path(self.path)
|
||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||
photopath = str(photopath)
|
||||
if not os.path.isfile(photopath):
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
elif self.live_photo and self.path and not self.ismissing:
|
||||
filename = pathlib.Path(self.path)
|
||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||
photopath = str(photopath)
|
||||
if not os.path.isfile(photopath):
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||
# TODO: should this be a warning or debug?
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
|
||||
return photopath
|
||||
|
||||
@@ -1119,21 +1115,239 @@ class PhotoInfo:
|
||||
self._owner = None
|
||||
return self._owner
|
||||
|
||||
def render_template(
|
||||
self, template_str: str, options: Optional[RenderOptions] = None
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
Args:
|
||||
template_str: a template string with fields to render
|
||||
options: a RenderOptions instance
|
||||
@property
|
||||
def score(self):
|
||||
"""Computed score information for a photo
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
ScoreInfo instance
|
||||
"""
|
||||
options = options or RenderOptions()
|
||||
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
||||
return template.render(template_str, options)
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"score not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||
except AttributeError:
|
||||
try:
|
||||
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=scores["overall_aesthetic"],
|
||||
curation=scores["curation"],
|
||||
promotion=scores["promotion"],
|
||||
highlight_visibility=scores["highlight_visibility"],
|
||||
behavioral=scores["behavioral"],
|
||||
failure=scores["failure"],
|
||||
harmonious_color=scores["harmonious_color"],
|
||||
immersiveness=scores["immersiveness"],
|
||||
interaction=scores["interaction"],
|
||||
interesting_subject=scores["interesting_subject"],
|
||||
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||
lively_color=scores["lively_color"],
|
||||
low_light=scores["low_light"],
|
||||
noise=scores["noise"],
|
||||
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||
pleasant_composition=scores["pleasant_composition"],
|
||||
pleasant_lighting=scores["pleasant_lighting"],
|
||||
pleasant_pattern=scores["pleasant_pattern"],
|
||||
pleasant_perspective=scores["pleasant_perspective"],
|
||||
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||
pleasant_reflection=scores["pleasant_reflection"],
|
||||
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||
tastefully_blurred=scores["tastefully_blurred"],
|
||||
well_chosen_subject=scores["well_chosen_subject"],
|
||||
well_framed_subject=scores["well_framed_subject"],
|
||||
well_timed_shot=scores["well_timed_shot"],
|
||||
)
|
||||
return self._scoreinfo
|
||||
except KeyError:
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=0.0,
|
||||
curation=0.0,
|
||||
promotion=0.0,
|
||||
highlight_visibility=0.0,
|
||||
behavioral=0.0,
|
||||
failure=0.0,
|
||||
harmonious_color=0.0,
|
||||
immersiveness=0.0,
|
||||
interaction=0.0,
|
||||
interesting_subject=0.0,
|
||||
intrusive_object_presence=0.0,
|
||||
lively_color=0.0,
|
||||
low_light=0.0,
|
||||
noise=0.0,
|
||||
pleasant_camera_tilt=0.0,
|
||||
pleasant_composition=0.0,
|
||||
pleasant_lighting=0.0,
|
||||
pleasant_pattern=0.0,
|
||||
pleasant_perspective=0.0,
|
||||
pleasant_post_processing=0.0,
|
||||
pleasant_reflection=0.0,
|
||||
pleasant_symmetry=0.0,
|
||||
sharply_focused_subject=0.0,
|
||||
tastefully_blurred=0.0,
|
||||
well_chosen_subject=0.0,
|
||||
well_framed_subject=0.0,
|
||||
well_timed_shot=0.0,
|
||||
)
|
||||
return self._scoreinfo
|
||||
|
||||
@property
|
||||
def search_info(self):
|
||||
"""returns SearchInfo object for photo
|
||||
only valid on Photos 5, on older libraries, returns None
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
# memoize SearchInfo object
|
||||
try:
|
||||
return self._search_info
|
||||
except AttributeError:
|
||||
self._search_info = SearchInfo(self)
|
||||
return self._search_info
|
||||
|
||||
@property
|
||||
def search_info_normalized(self):
|
||||
"""returns SearchInfo object for photo that produces normalized results
|
||||
only valid on Photos 5, on older libraries, returns None
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
# memoize SearchInfo object
|
||||
try:
|
||||
return self._search_info_normalized
|
||||
except AttributeError:
|
||||
self._search_info_normalized = SearchInfo(self, normalized=True)
|
||||
return self._search_info_normalized
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
"""returns list of labels applied to photo by Photos image categorization
|
||||
only valid on Photos 5, on older libraries returns empty list
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return []
|
||||
|
||||
return self.search_info.labels
|
||||
|
||||
@property
|
||||
def labels_normalized(self):
|
||||
"""returns normalized list of labels applied to photo by Photos image categorization
|
||||
only valid on Photos 5, on older libraries returns empty list
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return []
|
||||
|
||||
return self.search_info_normalized.labels
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
"""Returns list of Comment objects for any comments on the photo (sorted by date)"""
|
||||
try:
|
||||
return self._db._db_comments_uuid[self.uuid]["comments"]
|
||||
except:
|
||||
return []
|
||||
|
||||
@property
|
||||
def likes(self):
|
||||
"""Returns list of Like objects for any likes on the photo (sorted by date)"""
|
||||
try:
|
||||
return self._db._db_comments_uuid[self.uuid]["likes"]
|
||||
except:
|
||||
return []
|
||||
|
||||
@property
|
||||
def exif_info(self):
|
||||
"""Returns an ExifInfo object with the EXIF data for photo
|
||||
Note: the returned EXIF data is the data Photos stores in the database on import;
|
||||
ExifInfo does not provide access to the EXIF info in the actual image file
|
||||
Some or all of the fields may be None
|
||||
Only valid for Photos 5; on earlier database returns None
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"exif_info not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
exif = self._db._db_exifinfo_uuid[self.uuid]
|
||||
exif_info = ExifInfo(
|
||||
iso=exif["ZISO"],
|
||||
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
||||
metering_mode=exif["ZMETERINGMODE"],
|
||||
sample_rate=exif["ZSAMPLERATE"],
|
||||
track_format=exif["ZTRACKFORMAT"],
|
||||
white_balance=exif["ZWHITEBALANCE"],
|
||||
aperture=exif["ZAPERTURE"],
|
||||
bit_rate=exif["ZBITRATE"],
|
||||
duration=exif["ZDURATION"],
|
||||
exposure_bias=exif["ZEXPOSUREBIAS"],
|
||||
focal_length=exif["ZFOCALLENGTH"],
|
||||
fps=exif["ZFPS"],
|
||||
latitude=exif["ZLATITUDE"],
|
||||
longitude=exif["ZLONGITUDE"],
|
||||
shutter_speed=exif["ZSHUTTERSPEED"],
|
||||
camera_make=exif["ZCAMERAMAKE"],
|
||||
camera_model=exif["ZCAMERAMODEL"],
|
||||
codec=exif["ZCODEC"],
|
||||
lens_model=exif["ZLENSMODEL"],
|
||||
)
|
||||
except KeyError:
|
||||
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||
exif_info = ExifInfo(
|
||||
iso=None,
|
||||
flash_fired=None,
|
||||
metering_mode=None,
|
||||
sample_rate=None,
|
||||
track_format=None,
|
||||
white_balance=None,
|
||||
aperture=None,
|
||||
bit_rate=None,
|
||||
duration=None,
|
||||
exposure_bias=None,
|
||||
focal_length=None,
|
||||
fps=None,
|
||||
latitude=None,
|
||||
longitude=None,
|
||||
shutter_speed=None,
|
||||
camera_make=None,
|
||||
camera_model=None,
|
||||
codec=None,
|
||||
lens_model=None,
|
||||
)
|
||||
|
||||
return exif_info
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
"""Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
||||
Requires that exiftool (https://exiftool.org/) be installed
|
||||
If exiftool not installed, logs warning and returns None
|
||||
If photo path is missing, returns None
|
||||
"""
|
||||
try:
|
||||
# return the memoized instance if it exists
|
||||
return self._exiftool
|
||||
except AttributeError:
|
||||
try:
|
||||
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
||||
if self.path is not None and os.path.isfile(self.path):
|
||||
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
||||
else:
|
||||
exiftool = None
|
||||
except FileNotFoundError:
|
||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||
exiftool = None
|
||||
logging.warning(
|
||||
"exiftool not in path; download and install from https://exiftool.org/"
|
||||
)
|
||||
|
||||
self._exiftool = exiftool
|
||||
return self._exiftool
|
||||
|
||||
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
||||
"""Detects text in photo and returns lists of results as (detected text, confidence)
|
||||
@@ -1192,34 +1406,150 @@ class PhotoInfo:
|
||||
"""Returns latitude, in degrees"""
|
||||
return self._info["latitude"]
|
||||
|
||||
def _get_album_uuids(self):
|
||||
def render_template(
|
||||
self, template_str: str, options: Optional[RenderOptions] = None
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
Args:
|
||||
template_str: a template string with fields to render
|
||||
options: a RenderOptions instance
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
"""
|
||||
options = options or RenderOptions()
|
||||
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
||||
return template.render(template_str, options)
|
||||
|
||||
def export(
|
||||
self,
|
||||
dest,
|
||||
filename=None,
|
||||
edited=False,
|
||||
live_photo=False,
|
||||
raw_photo=False,
|
||||
export_as_hardlink=False,
|
||||
overwrite=False,
|
||||
increment=True,
|
||||
sidecar_json=False,
|
||||
sidecar_exiftool=False,
|
||||
sidecar_xmp=False,
|
||||
use_photos_export=False,
|
||||
timeout=120,
|
||||
exiftool=False,
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
render_options: Optional[RenderOptions] = None,
|
||||
):
|
||||
"""export photo
|
||||
dest: must be valid destination path (or exception raised)
|
||||
filename: (optional): name of exported picture; if not provided, will use current filename
|
||||
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
||||
For example, if photo is .CR2 file, edited image may be .jpeg.
|
||||
If you provide an extension different than what the actual file is,
|
||||
export will print a warning but will export the photo using the
|
||||
incorrect file extension (unless use_photos_export is true, in which case export will
|
||||
use the extension provided by Photos upon export; in this case, an incorrect extension is
|
||||
silently ignored).
|
||||
e.g. to get the extension of the edited photo,
|
||||
reference PhotoInfo.path_edited
|
||||
edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version
|
||||
(or raise exception if no edited version)
|
||||
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
||||
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
||||
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||
overwrite: (boolean, default=False); if True will overwrite files if they already exist
|
||||
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
|
||||
sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
|
||||
sidecar_exiftool: if set will write a json sidecar with data in format readable by exiftool
|
||||
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||
sidecar_xmp: if set will write an XMP sidecar with IPTC data
|
||||
sidecar filename will be dest/filename.xmp
|
||||
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
|
||||
timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
|
||||
returns list of full paths to the exported files
|
||||
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
|
||||
when exporting metadata with exiftool or sidecar
|
||||
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||
when exporting metadata with exiftool or sidecar
|
||||
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description
|
||||
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
|
||||
|
||||
Returns: list of photos exported
|
||||
"""
|
||||
|
||||
exporter = PhotoExporter(self)
|
||||
return exporter.export(
|
||||
dest=dest,
|
||||
filename=filename,
|
||||
edited=edited,
|
||||
live_photo=live_photo,
|
||||
raw_photo=raw_photo,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
increment=increment,
|
||||
sidecar_json=sidecar_json,
|
||||
sidecar_exiftool=sidecar_exiftool,
|
||||
sidecar_xmp=sidecar_xmp,
|
||||
use_photos_export=use_photos_export,
|
||||
timeout=timeout,
|
||||
exiftool=exiftool,
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
render_options=render_options,
|
||||
)
|
||||
|
||||
def _get_album_uuids(self, project=False):
|
||||
"""Return list of album UUIDs this photo is found in
|
||||
|
||||
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
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
version4 = True
|
||||
album_kind = [_PHOTOS_4_ALBUM_KIND]
|
||||
else:
|
||||
version4 = False
|
||||
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
|
||||
album_type = (
|
||||
[_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW]
|
||||
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 = []
|
||||
for album in self._info["albums"]:
|
||||
detail = self._db._dbalbum_details[album]
|
||||
if (
|
||||
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)
|
||||
)
|
||||
):
|
||||
if detail["kind"] in album_kind and not detail["intrash"]:
|
||||
album_list.append(album)
|
||||
return album_list
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""
|
||||
PhotoInfo class
|
||||
Represents a single photo in the Photos library and provides access to the photo's attributes
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
"""
|
||||
|
||||
from ._photoinfo_exifinfo import ExifInfo
|
||||
from ._photoinfo_export import ExportResults
|
||||
from ._photoinfo_scoreinfo import ScoreInfo
|
||||
from .photoinfo import PhotoInfo, PhotoInfoNone
|
||||
@@ -1,17 +0,0 @@
|
||||
""" PhotoInfo methods to expose comments and likes for shared photos """
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
""" Returns list of Comment objects for any comments on the photo (sorted by date) """
|
||||
try:
|
||||
return self._db._db_comments_uuid[self.uuid]["comments"]
|
||||
except:
|
||||
return []
|
||||
|
||||
@property
|
||||
def likes(self):
|
||||
""" Returns list of Like objects for any likes on the photo (sorted by date) """
|
||||
try:
|
||||
return self._db._db_comments_uuid[self.uuid]["likes"]
|
||||
except:
|
||||
return []
|
||||
@@ -1,94 +0,0 @@
|
||||
""" PhotoInfo methods to expose EXIF info from the library """
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .._constants import _PHOTOS_4_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExifInfo:
|
||||
""" EXIF info associated with a photo from the Photos library """
|
||||
|
||||
flash_fired: bool
|
||||
iso: int
|
||||
metering_mode: int
|
||||
sample_rate: int
|
||||
track_format: int
|
||||
white_balance: int
|
||||
aperture: float
|
||||
bit_rate: float
|
||||
duration: float
|
||||
exposure_bias: float
|
||||
focal_length: float
|
||||
fps: float
|
||||
latitude: float
|
||||
longitude: float
|
||||
shutter_speed: float
|
||||
camera_make: str
|
||||
camera_model: str
|
||||
codec: str
|
||||
lens_model: str
|
||||
|
||||
|
||||
@property
|
||||
def exif_info(self):
|
||||
""" Returns an ExifInfo object with the EXIF data for photo
|
||||
Note: the returned EXIF data is the data Photos stores in the database on import;
|
||||
ExifInfo does not provide access to the EXIF info in the actual image file
|
||||
Some or all of the fields may be None
|
||||
Only valid for Photos 5; on earlier database returns None
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"exif_info not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
exif = self._db._db_exifinfo_uuid[self.uuid]
|
||||
exif_info = ExifInfo(
|
||||
iso=exif["ZISO"],
|
||||
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
||||
metering_mode=exif["ZMETERINGMODE"],
|
||||
sample_rate=exif["ZSAMPLERATE"],
|
||||
track_format=exif["ZTRACKFORMAT"],
|
||||
white_balance=exif["ZWHITEBALANCE"],
|
||||
aperture=exif["ZAPERTURE"],
|
||||
bit_rate=exif["ZBITRATE"],
|
||||
duration=exif["ZDURATION"],
|
||||
exposure_bias=exif["ZEXPOSUREBIAS"],
|
||||
focal_length=exif["ZFOCALLENGTH"],
|
||||
fps=exif["ZFPS"],
|
||||
latitude=exif["ZLATITUDE"],
|
||||
longitude=exif["ZLONGITUDE"],
|
||||
shutter_speed=exif["ZSHUTTERSPEED"],
|
||||
camera_make=exif["ZCAMERAMAKE"],
|
||||
camera_model=exif["ZCAMERAMODEL"],
|
||||
codec=exif["ZCODEC"],
|
||||
lens_model=exif["ZLENSMODEL"],
|
||||
)
|
||||
except KeyError:
|
||||
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||
exif_info = ExifInfo(
|
||||
iso=None,
|
||||
flash_fired=None,
|
||||
metering_mode=None,
|
||||
sample_rate=None,
|
||||
track_format=None,
|
||||
white_balance=None,
|
||||
aperture=None,
|
||||
bit_rate=None,
|
||||
duration=None,
|
||||
exposure_bias=None,
|
||||
focal_length=None,
|
||||
fps=None,
|
||||
latitude=None,
|
||||
longitude=None,
|
||||
shutter_speed=None,
|
||||
camera_make=None,
|
||||
camera_model=None,
|
||||
codec=None,
|
||||
lens_model=None,
|
||||
)
|
||||
|
||||
return exif_info
|
||||
@@ -1,34 +0,0 @@
|
||||
""" Implementation for PhotoInfo.exiftool property which returns ExifTool object for a photo """
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..exiftool import ExifToolCaching, get_exiftool_path
|
||||
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
""" Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
||||
Requires that exiftool (https://exiftool.org/) be installed
|
||||
If exiftool not installed, logs warning and returns None
|
||||
If photo path is missing, returns None
|
||||
"""
|
||||
try:
|
||||
# return the memoized instance if it exists
|
||||
return self._exiftool
|
||||
except AttributeError:
|
||||
try:
|
||||
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
||||
if self.path is not None and os.path.isfile(self.path):
|
||||
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
||||
else:
|
||||
exiftool = None
|
||||
except FileNotFoundError:
|
||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||
exiftool = None
|
||||
logging.warning(
|
||||
f"exiftool not in path; download and install from https://exiftool.org/"
|
||||
)
|
||||
|
||||
self._exiftool = exiftool
|
||||
return self._exiftool
|
||||
@@ -1,119 +0,0 @@
|
||||
""" PhotoInfo methods to expose computed score info from the library """
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .._constants import _PHOTOS_4_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScoreInfo:
|
||||
""" Computed photo score info associated with a photo from the Photos library """
|
||||
|
||||
overall: float
|
||||
curation: float
|
||||
promotion: float
|
||||
highlight_visibility: float
|
||||
behavioral: float
|
||||
failure: float
|
||||
harmonious_color: float
|
||||
immersiveness: float
|
||||
interaction: float
|
||||
interesting_subject: float
|
||||
intrusive_object_presence: float
|
||||
lively_color: float
|
||||
low_light: float
|
||||
noise: float
|
||||
pleasant_camera_tilt: float
|
||||
pleasant_composition: float
|
||||
pleasant_lighting: float
|
||||
pleasant_pattern: float
|
||||
pleasant_perspective: float
|
||||
pleasant_post_processing: float
|
||||
pleasant_reflection: float
|
||||
pleasant_symmetry: float
|
||||
sharply_focused_subject: float
|
||||
tastefully_blurred: float
|
||||
well_chosen_subject: float
|
||||
well_framed_subject: float
|
||||
well_timed_shot: float
|
||||
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
""" Computed score information for a photo
|
||||
|
||||
Returns:
|
||||
ScoreInfo instance
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"score not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||
except AttributeError:
|
||||
try:
|
||||
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=scores["overall_aesthetic"],
|
||||
curation=scores["curation"],
|
||||
promotion=scores["promotion"],
|
||||
highlight_visibility=scores["highlight_visibility"],
|
||||
behavioral=scores["behavioral"],
|
||||
failure=scores["failure"],
|
||||
harmonious_color=scores["harmonious_color"],
|
||||
immersiveness=scores["immersiveness"],
|
||||
interaction=scores["interaction"],
|
||||
interesting_subject=scores["interesting_subject"],
|
||||
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||
lively_color=scores["lively_color"],
|
||||
low_light=scores["low_light"],
|
||||
noise=scores["noise"],
|
||||
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||
pleasant_composition=scores["pleasant_composition"],
|
||||
pleasant_lighting=scores["pleasant_lighting"],
|
||||
pleasant_pattern=scores["pleasant_pattern"],
|
||||
pleasant_perspective=scores["pleasant_perspective"],
|
||||
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||
pleasant_reflection=scores["pleasant_reflection"],
|
||||
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||
tastefully_blurred=scores["tastefully_blurred"],
|
||||
well_chosen_subject=scores["well_chosen_subject"],
|
||||
well_framed_subject=scores["well_framed_subject"],
|
||||
well_timed_shot=scores["well_timed_shot"],
|
||||
)
|
||||
return self._scoreinfo
|
||||
except KeyError:
|
||||
self._scoreinfo = ScoreInfo(
|
||||
overall=0.0,
|
||||
curation=0.0,
|
||||
promotion=0.0,
|
||||
highlight_visibility=0.0,
|
||||
behavioral=0.0,
|
||||
failure=0.0,
|
||||
harmonious_color=0.0,
|
||||
immersiveness=0.0,
|
||||
interaction=0.0,
|
||||
interesting_subject=0.0,
|
||||
intrusive_object_presence=0.0,
|
||||
lively_color=0.0,
|
||||
low_light=0.0,
|
||||
noise=0.0,
|
||||
pleasant_camera_tilt=0.0,
|
||||
pleasant_composition=0.0,
|
||||
pleasant_lighting=0.0,
|
||||
pleasant_pattern=0.0,
|
||||
pleasant_perspective=0.0,
|
||||
pleasant_post_processing=0.0,
|
||||
pleasant_reflection=0.0,
|
||||
pleasant_symmetry=0.0,
|
||||
sharply_focused_subject=0.0,
|
||||
tastefully_blurred=0.0,
|
||||
well_chosen_subject=0.0,
|
||||
well_framed_subject=0.0,
|
||||
well_timed_shot=0.0,
|
||||
)
|
||||
return self._scoreinfo
|
||||
@@ -30,6 +30,7 @@ import Photos
|
||||
import Quartz
|
||||
from Foundation import NSNotificationCenter, NSObject
|
||||
from PyObjCTools import AppHelper
|
||||
from wurlitzer import pipes
|
||||
|
||||
from .fileutil import FileUtil
|
||||
from .uti import get_preferred_uti_extension
|
||||
@@ -495,69 +496,73 @@ class PhotoAsset:
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
with pipes() as (out, err):
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
# will hold exported image data and needs to be cleaned up at end
|
||||
imagedata = None
|
||||
if raw:
|
||||
# export the raw component
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto:
|
||||
data = self._request_resource_data(resource)
|
||||
ext = pathlib.Path(self.raw_filename).suffix[1:]
|
||||
break
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
# will hold exported image data and needs to be cleaned up at end
|
||||
imagedata = None
|
||||
if raw:
|
||||
# export the raw component
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if (
|
||||
resource.type()
|
||||
== Photos.PHAssetResourceTypeAlternatePhoto
|
||||
):
|
||||
data = self._request_resource_data(resource)
|
||||
ext = pathlib.Path(self.raw_filename).suffix[1:]
|
||||
break
|
||||
else:
|
||||
raise PhotoKitExportError(
|
||||
"Could not get image data for RAW photo"
|
||||
)
|
||||
else:
|
||||
raise PhotoKitExportError(
|
||||
"Could not get image data for RAW photo"
|
||||
)
|
||||
else:
|
||||
# TODO: if user has selected use RAW as original, this returns the RAW
|
||||
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
|
||||
imagedata = self._request_image_data(version=version)
|
||||
if not imagedata.image_data:
|
||||
raise PhotoKitExportError("Could not get image data")
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
data = imagedata.image_data
|
||||
# TODO: if user has selected use RAW as original, this returns the RAW
|
||||
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
|
||||
imagedata = self._request_image_data(version=version)
|
||||
if not imagedata.image_data:
|
||||
raise PhotoKitExportError("Could not get image data")
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
data = imagedata.image_data
|
||||
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
|
||||
if imagedata:
|
||||
del imagedata
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
if imagedata:
|
||||
del imagedata
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
return [str(output_file)]
|
||||
|
||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
"""Request image data and metadata for self._phasset
|
||||
@@ -807,42 +812,46 @@ class VideoAsset(PhotoAsset):
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest, filename=filename, version=version, overwrite=overwrite
|
||||
)
|
||||
]
|
||||
with pipes() as (out, err):
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest,
|
||||
filename=filename,
|
||||
version=version,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
]
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
del videodata
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
del videodata
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
return [str(output_file)]
|
||||
|
||||
def _export_slow_mo(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
@@ -1053,64 +1062,69 @@ class LivePhotoAsset(PhotoAsset):
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
with pipes() as (out, err):
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
photo_ext = get_preferred_uti_extension(
|
||||
photo_resource.uniformTypeIdentifier()
|
||||
)
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(
|
||||
video_resource.uniformTypeIdentifier()
|
||||
)
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
del data
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
del data
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
request.dealloc()
|
||||
return exported
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
)
|
||||
|
||||
photo_ext = get_preferred_uti_extension(
|
||||
photo_resource.uniformTypeIdentifier()
|
||||
)
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(
|
||||
video_resource.uniformTypeIdentifier()
|
||||
)
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(
|
||||
increment_filename(photo_output_file)
|
||||
)
|
||||
video_output_file = pathlib.Path(
|
||||
increment_filename(video_output_file)
|
||||
)
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
del data
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
del data
|
||||
|
||||
request.dealloc()
|
||||
return exported
|
||||
|
||||
|
||||
class PhotoLibrary:
|
||||
|
||||
@@ -12,12 +12,14 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pprint import pformat
|
||||
from typing import List
|
||||
|
||||
import bitmath
|
||||
import photoscript
|
||||
from rich import print
|
||||
|
||||
from .._constants import (
|
||||
_DB_TABLE_NAMES,
|
||||
@@ -26,11 +28,15 @@ from .._constants import (
|
||||
_PHOTOS_3_VERSION,
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_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_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_TESTED_OS_VERSIONS,
|
||||
@@ -40,7 +46,7 @@ from .._constants import (
|
||||
TIME_DELTA,
|
||||
)
|
||||
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 ..fileutil import FileUtil
|
||||
from ..personinfo import PersonInfo
|
||||
@@ -250,6 +256,10 @@ class PhotosDB:
|
||||
# Dict to hold information on volume names (Photos 5+)
|
||||
self._db_filesystem_volumes = {}
|
||||
|
||||
# Dict to hold information on moments (Photos 5+)
|
||||
# key is Z_PK of ZMOMENT table and values are the moment info
|
||||
self._db_moment_pk = {}
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
@@ -423,7 +433,7 @@ class PhotosDB:
|
||||
for folder, detail in self._dbfolder_details.items()
|
||||
if not detail["intrash"]
|
||||
and not detail["isMagic"]
|
||||
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
and detail["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
]
|
||||
else:
|
||||
folders = [
|
||||
@@ -444,7 +454,7 @@ class PhotosDB:
|
||||
for folder in self._dbfolder_details.values()
|
||||
if not folder["intrash"]
|
||||
and not folder["isMagic"]
|
||||
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
and folder["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
]
|
||||
else:
|
||||
folder_names = [
|
||||
@@ -523,6 +533,18 @@ class PhotosDB:
|
||||
]
|
||||
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
|
||||
def db_version(self):
|
||||
"""return the database version as stored in LiGlobals table"""
|
||||
@@ -842,11 +864,10 @@ class PhotosDB:
|
||||
# build folder hierarchy
|
||||
for album, details in self._dbalbum_details.items():
|
||||
parent_folder = details["folderUuid"]
|
||||
if details[
|
||||
"albumSubclass"
|
||||
] == _PHOTOS_4_ALBUM_KIND and parent_folder not in [
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
]:
|
||||
if (
|
||||
details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
|
||||
and parent_folder not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
):
|
||||
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
|
||||
self._dbalbum_folders[album] = folder_hierarchy
|
||||
else:
|
||||
@@ -1576,7 +1597,7 @@ class PhotosDB:
|
||||
if parent_uuid is None:
|
||||
return folders
|
||||
|
||||
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM:
|
||||
if parent_uuid in _PHOTOS_4_TOP_LEVEL_ALBUMS:
|
||||
if not folders:
|
||||
# this is a top-level folder with no sub-folders
|
||||
folders = {uuid: None}
|
||||
@@ -2491,6 +2512,10 @@ class PhotosDB:
|
||||
verbose("Processing comments and likes for shared photos.")
|
||||
self._process_comments()
|
||||
|
||||
# process moments
|
||||
verbose("Processing moments.")
|
||||
self._process_moments()
|
||||
|
||||
# done processing, dump debug data if requested
|
||||
verbose("Done processing details from Photos library.")
|
||||
if _debug():
|
||||
@@ -2536,6 +2561,109 @@ class PhotosDB:
|
||||
logging.debug("Burst Photos (dbphotos_burst:")
|
||||
logging.debug(pformat(self._dbphotos_burst))
|
||||
|
||||
def _process_moments(self):
|
||||
"""Process data from ZMOMENT table"""
|
||||
# _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
raise NotImplementedError(
|
||||
f"Moment info implemented for this database version"
|
||||
)
|
||||
else:
|
||||
self._process_moment_5()
|
||||
|
||||
def _process_moment_5(self):
|
||||
"""Process moment info for Photos 5 databases"""
|
||||
|
||||
self._db_moment_pk = {}
|
||||
|
||||
results = self.execute(
|
||||
f"""
|
||||
SELECT
|
||||
Z_PK,
|
||||
ZTIMEZONEOFFSET,
|
||||
ZTRASHEDSTATE,
|
||||
ZAPPROXIMATELATITUDE,
|
||||
ZAPPROXIMATELONGITUDE,
|
||||
ZENDDATE,
|
||||
ZMODIFICATIONDATE,
|
||||
ZREPRESENTATIVEDATE,
|
||||
ZSTARTDATE,
|
||||
ZSUBTITLE,
|
||||
ZTITLE,
|
||||
ZUUID
|
||||
FROM ZMOMENT"""
|
||||
)
|
||||
|
||||
# results
|
||||
# 0 Z_PK,
|
||||
# 1 ZTIMEZONEOFFSET,
|
||||
# 2 ZTRASHEDSTATE,
|
||||
# 3 ZAPPROXIMATELATITUDE,
|
||||
# 4 ZAPPROXIMATELONGITUDE,
|
||||
# 5 ZENDDATE,
|
||||
# 6 ZMODIFICATIONDATE,
|
||||
# 7 ZREPRESENTATIVEDATE,
|
||||
# 8 ZSTARTDATE,
|
||||
# 9 ZSUBTITLE,
|
||||
# 10 ZTITLE,
|
||||
# 11 ZUUID
|
||||
|
||||
for row in results:
|
||||
moment_info = {}
|
||||
moment_info["pk"] = row[0]
|
||||
moment_info["timezoneOffset"] = row[1]
|
||||
moment_info["trashedState"] = row[2]
|
||||
moment_info["approximateLatitude"] = row[3]
|
||||
moment_info["approximateLongitude"] = row[4]
|
||||
moment_info["endDate"] = row[5]
|
||||
moment_info["modificationDate"] = row[6]
|
||||
moment_info["representativeDate"] = row[7]
|
||||
moment_info["startDate"] = row[8]
|
||||
moment_info["subtitle"] = row[9]
|
||||
moment_info["title"] = row[10]
|
||||
moment_info["uuid"] = row[11]
|
||||
|
||||
# if both lat/lon == -180, then it means location undefined
|
||||
if (
|
||||
moment_info["approximateLatitude"] == -180.0
|
||||
and moment_info["approximateLongitude"] == -180.0
|
||||
):
|
||||
moment_info["latitude"] = None
|
||||
moment_info["longitude"] = None
|
||||
else:
|
||||
moment_info["latitude"] = moment_info["approximateLatitude"]
|
||||
moment_info["longitude"] = moment_info["approximateLongitude"]
|
||||
|
||||
# process date stamps
|
||||
offset_seconds = moment_info["timezoneOffset"] or 0
|
||||
delta = timedelta(seconds=offset_seconds)
|
||||
tz = timezone(delta)
|
||||
for date_name in [
|
||||
"startDate",
|
||||
"endDate",
|
||||
"modificationDate",
|
||||
"representativeDate",
|
||||
]:
|
||||
date_stamp = moment_info[date_name]
|
||||
try:
|
||||
moment_date = datetime.fromtimestamp(date_stamp + TIME_DELTA)
|
||||
# save raw time stamp valu
|
||||
moment_info[date_name + "_timestamp"] = moment_info[date_name]
|
||||
moment_info[date_name] = moment_date.astimezone(tz=tz)
|
||||
except ValueError:
|
||||
# sometimes imageDate is invalid so use 1 Jan 1970 in UTC as image date
|
||||
moment_date = datetime(1970, 1, 1)
|
||||
tz = timezone(timedelta(0))
|
||||
moment_info[date_name + "_timestamp"] = date_stamp
|
||||
moment_info[date_name] = moment_date.astimezone(tz=tz)
|
||||
|
||||
# process title/subtitle
|
||||
moment_info["title"] = moment_info["title"] or ""
|
||||
moment_info["subtitle"] = moment_info["subtitle"] or ""
|
||||
|
||||
self._db_moment_pk[moment_info["pk"]] = moment_info
|
||||
|
||||
def _build_album_folder_hierarchy_5(self, uuid, folders=None):
|
||||
"""recursively build folder/album hierarchy
|
||||
uuid: uuid of the album/folder being processed
|
||||
@@ -2712,7 +2840,7 @@ class PhotosDB:
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
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
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
@@ -2720,20 +2848,21 @@ class PhotosDB:
|
||||
Args:
|
||||
shared: boolean; if True, returns shared albums, else normal 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
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: raised if mutually exclusive flags passed
|
||||
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if shared and import_session:
|
||||
if sum(bool(x) for x in [shared, import_session, project]) > 1:
|
||||
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:
|
||||
version4 = True
|
||||
if shared:
|
||||
logging.warning(
|
||||
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||
@@ -2744,16 +2873,44 @@ class PhotosDB:
|
||||
f"Import sessions not implemented for Photos library version {self._db_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
|
||||
else:
|
||||
version4 = False
|
||||
if shared:
|
||||
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
|
||||
elif import_session:
|
||||
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
|
||||
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 = []
|
||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||
@@ -2765,13 +2922,6 @@ class PhotosDB:
|
||||
(shared and detail["cloudownerhashedpersonid"] is not 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)
|
||||
return album_list
|
||||
@@ -3358,6 +3508,34 @@ class PhotosDB:
|
||||
# selection only works if photos selected in main media browser
|
||||
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:
|
||||
for function in options.function:
|
||||
photos = function[0](photos)
|
||||
@@ -3366,6 +3544,7 @@ class PhotosDB:
|
||||
|
||||
def execute(self, sql):
|
||||
"""Execute sql statement and return cursor"""
|
||||
self._db_connection, _ = self.get_db_connection()
|
||||
return self._db_connection.cursor().execute(sql)
|
||||
|
||||
def _duplicate_signature(self, uuid):
|
||||
|
||||
@@ -4,7 +4,11 @@ import logging
|
||||
import plistlib
|
||||
|
||||
from .._constants import (
|
||||
_PHOTOS_2_VERSION,
|
||||
_PHOTOS_3_VERSION,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_MODEL_VERSION,
|
||||
_PHOTOS_5_VERSION,
|
||||
_PHOTOS_6_MODEL_VERSION,
|
||||
_PHOTOS_7_MODEL_VERSION,
|
||||
_TESTED_DB_VERSIONS,
|
||||
@@ -83,3 +87,32 @@ def get_db_model_version(db_file):
|
||||
logging.warning(f"Unknown model version: {model_ver}")
|
||||
# cross our fingers and try latest version
|
||||
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}")
|
||||
|
||||
@@ -181,6 +181,9 @@ TEMPLATE_SUBSTITUTIONS_PATHLIB = {
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{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",
|
||||
"{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",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
||||
@@ -372,7 +375,9 @@ class PhotoTemplate:
|
||||
self.filepath = options.filepath
|
||||
self.quote = options.quote
|
||||
self.dest_path = options.dest_path
|
||||
self.exportdb = options.exportdb or ExportDBInMemory(None)
|
||||
self.exportdb = options.exportdb or ExportDBInMemory(
|
||||
None, self.export_dir or "."
|
||||
)
|
||||
|
||||
def render(
|
||||
self,
|
||||
@@ -1116,6 +1121,11 @@ class PhotoTemplate:
|
||||
values = []
|
||||
if field == "album":
|
||||
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":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -1126,13 +1136,15 @@ class PhotoTemplate:
|
||||
values = self.photo.labels
|
||||
elif field == "label_normalized":
|
||||
values = self.photo.labels_normalized
|
||||
elif field == "folder_album":
|
||||
elif field in ["folder_album", "folder_album_project"]:
|
||||
values = []
|
||||
# photos must be in an album to be in a folder
|
||||
if self.photo.burst:
|
||||
album_info = self.photo.burst_album_info
|
||||
else:
|
||||
album_info = self.photo.album_info
|
||||
if field == "folder_album_project":
|
||||
album_info += self.photo.project_info
|
||||
for album in album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
@@ -1193,7 +1205,7 @@ class PhotoTemplate:
|
||||
elif isinstance(obj, (str, int, float)):
|
||||
values = [str(obj)]
|
||||
else:
|
||||
values = [val for val in obj]
|
||||
values = list(obj)
|
||||
elif field == "detected_text":
|
||||
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
|
||||
else:
|
||||
@@ -1202,7 +1214,7 @@ class PhotoTemplate:
|
||||
# sanitize directory names if needed, folder_album handled differently above
|
||||
if self.filename:
|
||||
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
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ class QueryOptions:
|
||||
no_location: Optional[bool] = None
|
||||
function: Optional[List[Tuple[callable, str]]] = None
|
||||
selected: Optional[bool] = None
|
||||
exif: Optional[Iterable[Tuple[str, str]]] = None
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
39
osxphotos/scoreinfo.py
Normal file
@@ -0,0 +1,39 @@
|
||||
""" ScoreInfo class to expose computed score info from the library """
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ._constants import _PHOTOS_4_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScoreInfo:
|
||||
""" Computed photo score info associated with a photo from the Photos library """
|
||||
|
||||
overall: float
|
||||
curation: float
|
||||
promotion: float
|
||||
highlight_visibility: float
|
||||
behavioral: float
|
||||
failure: float
|
||||
harmonious_color: float
|
||||
immersiveness: float
|
||||
interaction: float
|
||||
interesting_subject: float
|
||||
intrusive_object_presence: float
|
||||
lively_color: float
|
||||
low_light: float
|
||||
noise: float
|
||||
pleasant_camera_tilt: float
|
||||
pleasant_composition: float
|
||||
pleasant_lighting: float
|
||||
pleasant_pattern: float
|
||||
pleasant_perspective: float
|
||||
pleasant_post_processing: float
|
||||
pleasant_reflection: float
|
||||
pleasant_symmetry: float
|
||||
sharply_focused_subject: float
|
||||
tastefully_blurred: float
|
||||
well_chosen_subject: float
|
||||
well_framed_subject: float
|
||||
well_timed_shot: float
|
||||
|
||||
@@ -1,98 +1,39 @@
|
||||
""" Methods and class for PhotoInfo exposing SearchInfo data such as labels
|
||||
Adds the following properties to PhotoInfo (valid only for Photos 5):
|
||||
search_info: returns a SearchInfo object
|
||||
search_info_normalized: returns a SearchInfo object with properties that produce normalized results
|
||||
labels: returns list of labels
|
||||
labels_normalized: returns list of normalized labels
|
||||
""" class for PhotoInfo exposing SearchInfo data such as labels
|
||||
"""
|
||||
|
||||
from .._constants import (
|
||||
from ._constants import (
|
||||
_PHOTOS_4_VERSION,
|
||||
SEARCH_CATEGORY_ACTIVITY,
|
||||
SEARCH_CATEGORY_ALL_LOCALITY,
|
||||
SEARCH_CATEGORY_BODY_OF_WATER,
|
||||
SEARCH_CATEGORY_CITY,
|
||||
SEARCH_CATEGORY_COUNTRY,
|
||||
SEARCH_CATEGORY_HOLIDAY,
|
||||
SEARCH_CATEGORY_LABEL,
|
||||
SEARCH_CATEGORY_MEDIA_TYPES,
|
||||
SEARCH_CATEGORY_MONTH,
|
||||
SEARCH_CATEGORY_NEIGHBORHOOD,
|
||||
SEARCH_CATEGORY_PLACE_NAME,
|
||||
SEARCH_CATEGORY_STREET,
|
||||
SEARCH_CATEGORY_ALL_LOCALITY,
|
||||
SEARCH_CATEGORY_COUNTRY,
|
||||
SEARCH_CATEGORY_SEASON,
|
||||
SEARCH_CATEGORY_STATE,
|
||||
SEARCH_CATEGORY_STATE_ABBREVIATION,
|
||||
SEARCH_CATEGORY_BODY_OF_WATER,
|
||||
SEARCH_CATEGORY_MONTH,
|
||||
SEARCH_CATEGORY_YEAR,
|
||||
SEARCH_CATEGORY_HOLIDAY,
|
||||
SEARCH_CATEGORY_ACTIVITY,
|
||||
SEARCH_CATEGORY_SEASON,
|
||||
SEARCH_CATEGORY_STREET,
|
||||
SEARCH_CATEGORY_VENUE,
|
||||
SEARCH_CATEGORY_VENUE_TYPE,
|
||||
SEARCH_CATEGORY_MEDIA_TYPES,
|
||||
SEARCH_CATEGORY_YEAR,
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def search_info(self):
|
||||
""" returns SearchInfo object for photo
|
||||
only valid on Photos 5, on older libraries, returns None
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
# memoize SearchInfo object
|
||||
try:
|
||||
return self._search_info
|
||||
except AttributeError:
|
||||
self._search_info = SearchInfo(self)
|
||||
return self._search_info
|
||||
|
||||
|
||||
@property
|
||||
def search_info_normalized(self):
|
||||
""" returns SearchInfo object for photo that produces normalized results
|
||||
only valid on Photos 5, on older libraries, returns None
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
# memoize SearchInfo object
|
||||
try:
|
||||
return self._search_info_normalized
|
||||
except AttributeError:
|
||||
self._search_info_normalized = SearchInfo(self, normalized=True)
|
||||
return self._search_info_normalized
|
||||
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
""" returns list of labels applied to photo by Photos image categorization
|
||||
only valid on Photos 5, on older libraries returns empty list
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return []
|
||||
|
||||
return self.search_info.labels
|
||||
|
||||
|
||||
@property
|
||||
def labels_normalized(self):
|
||||
""" returns normalized list of labels applied to photo by Photos image categorization
|
||||
only valid on Photos 5, on older libraries returns empty list
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return []
|
||||
|
||||
return self.search_info_normalized.labels
|
||||
|
||||
|
||||
class SearchInfo:
|
||||
""" Info about search terms such as machine learning labels that Photos knows about a photo """
|
||||
"""Info about search terms such as machine learning labels that Photos knows about a photo"""
|
||||
|
||||
def __init__(self, photo, normalized=False):
|
||||
""" photo: PhotoInfo object
|
||||
normalized: if True, all properties return normalized (lower case) results """
|
||||
"""photo: PhotoInfo object
|
||||
normalized: if True, all properties return normalized (lower case) results"""
|
||||
|
||||
if photo._db._db_version <= _PHOTOS_4_VERSION:
|
||||
raise NotImplementedError(
|
||||
f"search info not implemented for this database version"
|
||||
"search info not implemented for this database version"
|
||||
)
|
||||
|
||||
self._photo = photo
|
||||
@@ -107,27 +48,27 @@ class SearchInfo:
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
""" return list of labels associated with Photo """
|
||||
"""return list of labels associated with Photo"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
|
||||
|
||||
@property
|
||||
def place_names(self):
|
||||
""" returns list of place names """
|
||||
"""returns list of place names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
|
||||
|
||||
@property
|
||||
def streets(self):
|
||||
""" returns list of street names """
|
||||
"""returns list of street names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
|
||||
|
||||
@property
|
||||
def neighborhoods(self):
|
||||
""" returns list of neighborhoods """
|
||||
"""returns list of neighborhoods"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
|
||||
|
||||
@property
|
||||
def locality_names(self):
|
||||
""" returns list of other locality names """
|
||||
"""returns list of other locality names"""
|
||||
locality = []
|
||||
for category in SEARCH_CATEGORY_ALL_LOCALITY:
|
||||
locality += self._get_text_for_category(category)
|
||||
@@ -135,74 +76,74 @@ class SearchInfo:
|
||||
|
||||
@property
|
||||
def city(self):
|
||||
""" returns city/town """
|
||||
"""returns city/town"""
|
||||
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
|
||||
return city[0] if city else ""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" returns state name """
|
||||
"""returns state name"""
|
||||
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
|
||||
return state[0] if state else ""
|
||||
|
||||
@property
|
||||
def state_abbreviation(self):
|
||||
""" returns state abbreviation """
|
||||
"""returns state abbreviation"""
|
||||
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
|
||||
return abbrev[0] if abbrev else ""
|
||||
|
||||
@property
|
||||
def country(self):
|
||||
""" returns country name """
|
||||
"""returns country name"""
|
||||
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
|
||||
return country[0] if country else ""
|
||||
|
||||
@property
|
||||
def month(self):
|
||||
""" returns month name """
|
||||
"""returns month name"""
|
||||
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
|
||||
return month[0] if month else ""
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
""" returns year """
|
||||
"""returns year"""
|
||||
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
|
||||
return year[0] if year else ""
|
||||
|
||||
@property
|
||||
def bodies_of_water(self):
|
||||
""" returns list of body of water names """
|
||||
"""returns list of body of water names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
|
||||
|
||||
@property
|
||||
def holidays(self):
|
||||
""" returns list of holiday names """
|
||||
"""returns list of holiday names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
|
||||
|
||||
@property
|
||||
def activities(self):
|
||||
""" returns list of activity names """
|
||||
"""returns list of activity names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
|
||||
|
||||
@property
|
||||
def season(self):
|
||||
""" returns season name """
|
||||
"""returns season name"""
|
||||
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
|
||||
return season[0] if season else ""
|
||||
|
||||
@property
|
||||
def venues(self):
|
||||
""" returns list of venue names """
|
||||
"""returns list of venue names"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
|
||||
|
||||
@property
|
||||
def venue_types(self):
|
||||
""" returns list of venue types """
|
||||
"""returns list of venue types"""
|
||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
|
||||
|
||||
@property
|
||||
def media_types(self):
|
||||
""" returns list of media types (photo, video, panorama, etc) """
|
||||
"""returns list of media types (photo, video, panorama, etc)"""
|
||||
types = []
|
||||
for category in SEARCH_CATEGORY_MEDIA_TYPES:
|
||||
types += self._get_text_for_category(category)
|
||||
@@ -210,7 +151,7 @@ class SearchInfo:
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
""" return all search info properties in a single list """
|
||||
"""return all search info properties in a single list"""
|
||||
all = (
|
||||
self.labels
|
||||
+ self.place_names
|
||||
@@ -242,7 +183,7 @@ class SearchInfo:
|
||||
return all
|
||||
|
||||
def asdict(self):
|
||||
""" return dict of search info """
|
||||
"""return dict of search info"""
|
||||
return {
|
||||
"labels": self.labels,
|
||||
"place_names": self.place_names,
|
||||
@@ -265,7 +206,7 @@ class SearchInfo:
|
||||
}
|
||||
|
||||
def _get_text_for_category(self, category):
|
||||
""" return list of text for a specified category ID """
|
||||
"""return list of text for a specified category ID"""
|
||||
if self._db_searchinfo:
|
||||
content = "normalized_string" if self._normalized else "content_string"
|
||||
return [
|
||||
@@ -528,35 +528,38 @@ def _get_uti_from_mdls(extension):
|
||||
# mdls -name kMDItemContentType foo.3fr
|
||||
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"/usr/bin/mdls",
|
||||
"-name",
|
||||
"kMDItemContentType",
|
||||
temp.name,
|
||||
]
|
||||
).splitlines()
|
||||
output = output[0].decode("UTF-8") if output else None
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"/usr/bin/mdls",
|
||||
"-name",
|
||||
"kMDItemContentType",
|
||||
temp.name,
|
||||
]
|
||||
).splitlines()
|
||||
output = output[0].decode("UTF-8") if output else None
|
||||
if not output:
|
||||
return None
|
||||
|
||||
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_uti_from_ext_dict(ext):
|
||||
try:
|
||||
return EXT_UTI_DICT[ext]
|
||||
return EXT_UTI_DICT[ext.lower()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def _get_ext_from_uti_dict(uti):
|
||||
try:
|
||||
return UTI_EXT_DICT[uti]
|
||||
return UTI_EXT_DICT[uti.lower()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Click>=8.0.1,<9.0
|
||||
Mako>=1.1.4,<1.2.0
|
||||
PyYAML>=5.4.1<5.5.0
|
||||
PyYAML>=5.4.1,<6.0.0
|
||||
bitmath>=1.3.3.1,<1.4.0.0
|
||||
bpylist2==3.0.2
|
||||
dataclasses==0.7;python_version<'3.7'
|
||||
@@ -10,16 +10,16 @@ osxmetadata>=0.99.34,<1.0.0
|
||||
pathvalidate>=2.4.1,<2.5.0
|
||||
photoscript>=0.1.4,<0.2.0
|
||||
ptpython>=3.0.20,<3.1.0
|
||||
pyobjc-core>=7.2,<8.0
|
||||
pyobjc-framework-AVFoundation>=7.2,<8.0
|
||||
pyobjc-framework-AppleScriptKit>=7.2,<8.0
|
||||
pyobjc-framework-AppleScriptObjC>=7.2,<8.0
|
||||
pyobjc-framework-Cocoa>=7.2,<8.0
|
||||
pyobjc-framework-CoreServices>=7.2,<8.0
|
||||
pyobjc-framework-Metal>=7.2,<8.0
|
||||
pyobjc-framework-Photos>=7.2,<8.0
|
||||
pyobjc-framework-Quartz>=7.2,<8.0
|
||||
pyobjc-framework-Vision>=7.2,<8.0
|
||||
pyobjc-core>=7.3,<9.0
|
||||
pyobjc-framework-AVFoundation>=7.3,<9.0
|
||||
pyobjc-framework-AppleScriptKit>=7.3,<9.0
|
||||
pyobjc-framework-AppleScriptObjC>=7.3,<9.0
|
||||
pyobjc-framework-Cocoa>=7.3,<9.0
|
||||
pyobjc-framework-CoreServices>=7.2,<9.0
|
||||
pyobjc-framework-Metal>=7.3,<9.0
|
||||
pyobjc-framework-Photos>=7.3,<9.0
|
||||
pyobjc-framework-Quartz>=7.3,<9.0
|
||||
pyobjc-framework-Vision>=7.3,<9.0
|
||||
rich>=10.6.0,<=11.0.0
|
||||
textx>=2.3.0,<2.4.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
build
|
||||
m2r2
|
||||
pyinstaller==4.4
|
||||
pytest-mock
|
||||
pytest==6.2.4
|
||||
sphinx_click
|
||||
sphinx_rtd_theme
|
||||
twine
|
||||
wheel
|
||||
20
setup.py
@@ -86,16 +86,16 @@ setup(
|
||||
"pathvalidate>=2.4.1,<3.0.0",
|
||||
"photoscript>=0.1.4,<0.2.0",
|
||||
"ptpython>=3.0.20,<4.0.0",
|
||||
"pyobjc-core>=7.2,<8.0",
|
||||
"pyobjc-framework-AVFoundation>=7.2,<8.0",
|
||||
"pyobjc-framework-AppleScriptKit>=7.2,<8.0",
|
||||
"pyobjc-framework-AppleScriptObjC>=7.2,<8.0",
|
||||
"pyobjc-framework-Cocoa>=7.2,<8.0",
|
||||
"pyobjc-framework-CoreServices>=7.2,<8.0",
|
||||
"pyobjc-framework-Metal>=7.2,<8.0",
|
||||
"pyobjc-framework-Photos>=7.2,<8.0",
|
||||
"pyobjc-framework-Quartz>=7.2,<8.0",
|
||||
"pyobjc-framework-Vision>=7.2,<8.0",
|
||||
"pyobjc-core>=7.3,<9.0",
|
||||
"pyobjc-framework-AVFoundation>=7.3,<9.0",
|
||||
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
|
||||
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
|
||||
"pyobjc-framework-Cocoa>=7.3,<9.0",
|
||||
"pyobjc-framework-CoreServices>=7.2,<9.0",
|
||||
"pyobjc-framework-Metal>=7.3,<9.0",
|
||||
"pyobjc-framework-Photos>=7.3,<9.0",
|
||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||
"rich>=10.6.0,<=11.0.0",
|
||||
"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/Test-12.0.1.photoslibrary/database/Photos.sqlite
Normal file
BIN
tests/Test-12.0.1.photoslibrary/database/Photos.sqlite-shm
Normal file
BIN
tests/Test-12.0.1.photoslibrary/database/Photos.sqlite-wal
Normal file
16
tests/Test-12.0.1.photoslibrary/database/Photos.sqlite.lock
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>nshome-mini-m1.local</string>
|
||||
<key>hostuuid</key>
|
||||
<string>0121963E-0177-5496-B15B-ECE62F36DF95</string>
|
||||
<key>pid</key>
|
||||
<integer>1335</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
<integer>501</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
tests/Test-12.0.1.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/Test-12.0.1.photoslibrary/database/photos.db
Normal file
0
tests/Test-12.0.1.photoslibrary/database/protection
Normal file
BIN
tests/Test-12.0.1.photoslibrary/database/search/psi.sqlite
Normal file
BIN
tests/Test-12.0.1.photoslibrary/database/search/psi.sqlite-shm
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: 574 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 541 KiB |
@@ -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>MigrationService</key>
|
||||
<dict>
|
||||
<key>State</key>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>MigrationService.LastCompletedTask</key>
|
||||
<integer>12</integer>
|
||||
<key>MigrationService.ValidationCounts</key>
|
||||
<dict>
|
||||
<key>MigrationDetectedFaceprint</key>
|
||||
<integer>6</integer>
|
||||
<key>MigrationManagedAsset</key>
|
||||
<integer>0</integer>
|
||||
<key>MigrationSceneClassification</key>
|
||||
<integer>44</integer>
|
||||
<key>MigrationUnmanagedAdjustment</key>
|
||||
<integer>0</integer>
|
||||
<key>RDVersion.cloudLocalState.CPLIsNotPushed</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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/>
|
||||
<key>ExpandedSidebarItemIdentifiers</key>
|
||||
<array>
|
||||
<string>92D68107-B6C7-453B-96D2-97B0F26D5B8B/L0/020</string>
|
||||
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
|
||||
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
|
||||
<string>2C2AF115-BD1D-4434-A747-D1C8BD8E2045/L0/020</string>
|
||||
<string>CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020</string>
|
||||
</array>
|
||||
<key>IPXWorkspaceControllerZoomLevelsKey</key>
|
||||
<dict>
|
||||
<key>kZoomLevelIdentifierPhotosGrid</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Photos</key>
|
||||
<dict>
|
||||
<key>CollapsedSidebarSectionIdentifiers</key>
|
||||
<array/>
|
||||
<key>ExpandedSidebarItemIdentifiers</key>
|
||||
<array>
|
||||
<string>TopLevelAlbums</string>
|
||||
<string>TopLevelSlideshows</string>
|
||||
</array>
|
||||
<key>IPXWorkspaceControllerZoomLevelsKey</key>
|
||||
<dict>
|
||||
<key>kZoomLevelIdentifierAlbums</key>
|
||||
<integer>7</integer>
|
||||
<key>kZoomLevelIdentifierVersions</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>lastAddToDestination</key>
|
||||
<dict>
|
||||
<key>key</key>
|
||||
<integer>1</integer>
|
||||
<key>lastKnownDisplayName</key>
|
||||
<string>September 28, 2018</string>
|
||||
<key>type</key>
|
||||
<string>album</string>
|
||||
<key>uuid</key>
|
||||
<string>DFFKmHt3Tk+AGzZLe2Xq+g</string>
|
||||
</dict>
|
||||
<key>lastKnownItemCounts</key>
|
||||
<dict>
|
||||
<key>other</key>
|
||||
<integer>0</integer>
|
||||
<key>photos</key>
|
||||
<integer>7</integer>
|
||||
<key>videos</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</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>FaceProcessingInternalVersion</key>
|
||||
<integer>11</integer>
|
||||
</dict>
|
||||
</plist>
|
||||