Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c75ff17c | ||
|
|
1e58e20b3c | ||
|
|
51ca4d30f3 | ||
|
|
15b331047c | ||
|
|
81f4a4c3ee | ||
|
|
227a3e2836 | ||
|
|
0688729785 | ||
|
|
8fa7f18ece | ||
|
|
99aff7396f | ||
|
|
c36610ab62 | ||
|
|
2aa80996bf | ||
|
|
de8f29a431 | ||
|
|
c383212822 | ||
|
|
5fc8c022ab | ||
|
|
8aa850e969 | ||
|
|
3dda7371ab | ||
|
|
4b3433fc20 | ||
|
|
106b258c6d | ||
|
|
8caee5a81b | ||
|
|
6788f318a2 | ||
|
|
1f7480a9b9 | ||
|
|
8b72374001 | ||
|
|
cf72a1f821 | ||
|
|
11266cd62b | ||
|
|
1a5bb0e36a | ||
|
|
a1a97eec13 | ||
|
|
eb0251b198 | ||
|
|
dbdeb069be | ||
|
|
1d5b51dd3d | ||
|
|
45392511e5 | ||
|
|
e76b2cfadc | ||
|
|
d6e0e340b7 | ||
|
|
d518ca5d5d | ||
|
|
a6cce9ef65 | ||
|
|
00481d3623 | ||
|
|
f5ed3d7518 | ||
|
|
c091a0b6c1 | ||
|
|
5f298709d7 | ||
|
|
9a9a1be165 | ||
|
|
89f82e92f0 | ||
|
|
5bd793ae64 | ||
|
|
de584e3dec | ||
|
|
a8586f911f | ||
|
|
00796f1c0a | ||
|
|
1b89f85a41 | ||
|
|
0472582870 | ||
|
|
830da7b3b4 | ||
|
|
5e7ab06458 | ||
|
|
b2b814954b | ||
|
|
8b9af7be67 | ||
|
|
a3aee63eab | ||
|
|
aeb6283b2b | ||
|
|
47e2454584 | ||
|
|
ee370f5dfb | ||
|
|
3e2076df12 | ||
|
|
2afab9e3b1 | ||
|
|
3c8d7e13b9 | ||
|
|
c3bd04f257 | ||
|
|
12fecec3de | ||
|
|
e4faf3779c | ||
|
|
53a61ed5aa | ||
|
|
debc001af9 | ||
|
|
025ee36086 | ||
|
|
d66cb6dc2b | ||
|
|
88e56bc0b9 | ||
|
|
327f19809e | ||
|
|
924a5f2f61 | ||
|
|
3557658b73 | ||
|
|
bb65765afa | ||
|
|
f8d8028631 | ||
|
|
cad4e1eeff | ||
|
|
ce5145ff85 | ||
|
|
6bf24ad2de | ||
|
|
d6fc8fc3b1 | ||
|
|
003531b052 | ||
|
|
e673ab64ce | ||
|
|
9ed1b394a9 | ||
|
|
40de05c5fd | ||
|
|
f610d3cc65 | ||
|
|
d0232284f0 |
@@ -261,7 +261,8 @@
|
||||
"contributions": [
|
||||
"bug",
|
||||
"ideas",
|
||||
"test"
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -395,6 +396,74 @@
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zephyr325",
|
||||
"name": "zephyr325",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5245609?v=4",
|
||||
"profile": "https://github.com/zephyr325",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "drodner",
|
||||
"name": "drodner",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10236892?v=4",
|
||||
"profile": "https://github.com/drodner",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fmckeogh",
|
||||
"name": "Ferdia McKeogh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8290136?v=4",
|
||||
"profile": "https://fmckeogh.github.io",
|
||||
"contributions": [
|
||||
"code",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PetrochukM",
|
||||
"name": "Michael Petrochuk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7424737?v=4",
|
||||
"profile": "https://wellsaidlabs.com",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "qkeddy",
|
||||
"name": "Quin Eddy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9737814?v=4",
|
||||
"profile": "https://qkeddy.github.io/quin-eddy-development-portfolio/",
|
||||
"contributions": [
|
||||
"ideas",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "johnsturgeon",
|
||||
"name": "John Sturgeon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9746310?v=4",
|
||||
"profile": "https://github.com/johnsturgeon",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mave2k",
|
||||
"name": "mave2k",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8629837?v=4",
|
||||
"profile": "https://github.com/mave2k",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.54.2
|
||||
current_version = 0.56.2
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
serialize = {major}.{minor}.{patch}
|
||||
|
||||
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.py linguist-detectable=true
|
||||
*.js linguist-detectable=false
|
||||
*.html linguist-detectable=false
|
||||
*.css linguist-detectable=false
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
||||
python-version: ['3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -974,6 +974,10 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
|
||||
|
||||
Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates.
|
||||
|
||||
#### `fingerprint`
|
||||
|
||||
Returns a unique fingerprint for the original photo file. This is a hash of the original photo file and is useful for finding duplicates or correlating photos across multiple libraries.
|
||||
|
||||
#### `hexdigest`
|
||||
|
||||
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
|
||||
@@ -2012,7 +2016,7 @@ cog.out(get_template_field_table())
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|A carriage return + line feed: '\r\n'|
|
||||
|{tab}|:A tab: '\t'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.54.2'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.56.2'|
|
||||
|{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|
|
||||
@@ -2237,7 +2241,7 @@ def main():
|
||||
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.album_names)
|
||||
print(photosdb.albums)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
|
||||
122
CHANGELOG.md
122
CHANGELOG.md
@@ -4,6 +4,128 @@ 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.56.1](https://github.com/RhetTbull/osxphotos/compare/v0.56.0...v0.56.1)
|
||||
|
||||
> 14 January 2023
|
||||
|
||||
- Release 0.56.1 [`#922`](https://github.com/RhetTbull/osxphotos/pull/922)
|
||||
- Feature add sync command 887 [`#921`](https://github.com/RhetTbull/osxphotos/pull/921)
|
||||
- Updated docs [`2aa8099`](https://github.com/RhetTbull/osxphotos/commit/2aa80996bf2934c8b7e72ec491c8877e4d3b4d5f)
|
||||
- Updated docs [`c36610a`](https://github.com/RhetTbull/osxphotos/commit/c36610ab622cfb3051f43280c715aa65855b1715)
|
||||
|
||||
#### [v0.56.0](https://github.com/RhetTbull/osxphotos/compare/v0.55.7...v0.56.0)
|
||||
|
||||
> 13 January 2023
|
||||
|
||||
- Release 0 56 0 [`#919`](https://github.com/RhetTbull/osxphotos/pull/919)
|
||||
- Release 0.56.0 [`#918`](https://github.com/RhetTbull/osxphotos/pull/918)
|
||||
- Added --profile, --watch, --breakpoint, --debug as global options [`#917`](https://github.com/RhetTbull/osxphotos/pull/917)
|
||||
- add oPromessa as a contributor for code [`#914`](https://github.com/RhetTbull/osxphotos/pull/914)
|
||||
- Added --incloud, --not-incloud, --not-missing, --cloudasset, --not-cloudasset to query options, #800 [`#902`](https://github.com/RhetTbull/osxphotos/pull/902)
|
||||
- Added PhotoInfo.fingerprint #900 [`#901`](https://github.com/RhetTbull/osxphotos/pull/901)
|
||||
- add johnsturgeon as a contributor for bug, and doc [`#898`](https://github.com/RhetTbull/osxphotos/pull/898)
|
||||
- add qkeddy as a contributor for ideas, and data [`#895`](https://github.com/RhetTbull/osxphotos/pull/895)
|
||||
- Fixed API docs and added example, #897 [`8b72374`](https://github.com/RhetTbull/osxphotos/commit/8b72374001caae6449f1000cf9d38250d84fedbe)
|
||||
- Added .gitattributes [`1f7480a`](https://github.com/RhetTbull/osxphotos/commit/1f7480a9b9047316adecbfd9523cbb3ef101f82a)
|
||||
|
||||
#### [v0.55.7](https://github.com/RhetTbull/osxphotos/compare/v0.55.6...v0.55.7)
|
||||
|
||||
> 1 January 2023
|
||||
|
||||
- Release files for 0.55.7 [`#894`](https://github.com/RhetTbull/osxphotos/pull/894)
|
||||
- Fix for incorrect path for shared photos on Ventura, #883 [`#893`](https://github.com/RhetTbull/osxphotos/pull/893)
|
||||
|
||||
#### [v0.55.6](https://github.com/RhetTbull/osxphotos/compare/v0.55.5...v0.55.6)
|
||||
|
||||
> 30 December 2022
|
||||
|
||||
- Added Quicktime:ContentCreateDate to photo exporter #890 [`#891`](https://github.com/RhetTbull/osxphotos/pull/891)
|
||||
- Use "QuickTime:ContentCreateDate" [`#888`](https://github.com/RhetTbull/osxphotos/pull/888)
|
||||
- add PetrochukM as a contributor for bug, and code [`#889`](https://github.com/RhetTbull/osxphotos/pull/889)
|
||||
- Release files [`1d5b51d`](https://github.com/RhetTbull/osxphotos/commit/1d5b51dd3d027ed80367954381cfe7d1b1e514ec)
|
||||
- Added examples for finding / fixing bad extensions, #382, #336 [`00481d3`](https://github.com/RhetTbull/osxphotos/commit/00481d3623885eb6ff30d895cc32e5cfc3f16076)
|
||||
- Version bump for release [`c091a0b`](https://github.com/RhetTbull/osxphotos/commit/c091a0b6c10feabb4165bf244041d8c9a0e8ff89)
|
||||
- Fixed color output for find_bad_extensions.py [`a6cce9e`](https://github.com/RhetTbull/osxphotos/commit/a6cce9ef659c8d70eb3426a5c2d220edb29eafcb)
|
||||
- Added about string to kvstore [`d518ca5`](https://github.com/RhetTbull/osxphotos/commit/d518ca5d5d6a0613b73dd087ad9bbc548da78af1)
|
||||
|
||||
#### [v0.55.5](https://github.com/RhetTbull/osxphotos/compare/v0.55.3...v0.55.5)
|
||||
|
||||
> 24 December 2022
|
||||
|
||||
- Handle "Z" as EXIF offset time [`#881`](https://github.com/RhetTbull/osxphotos/pull/881)
|
||||
- add fmckeogh as a contributor for code, and bug [`#882`](https://github.com/RhetTbull/osxphotos/pull/882)
|
||||
- Version bump for release [`5f29870`](https://github.com/RhetTbull/osxphotos/commit/5f298709d7d87f00d0abf6401a6cb101a7ebe630)
|
||||
|
||||
#### [v0.55.3](https://github.com/RhetTbull/osxphotos/compare/v0.55.2...v0.55.3)
|
||||
|
||||
> 19 December 2022
|
||||
|
||||
- Release files for 0.55.3 [`#879`](https://github.com/RhetTbull/osxphotos/pull/879)
|
||||
- Partial implementation for #868, candidate paths [`#878`](https://github.com/RhetTbull/osxphotos/pull/878)
|
||||
- Fix for #853, deleted files not in exportdb --report [`#877`](https://github.com/RhetTbull/osxphotos/pull/877)
|
||||
- Fix for #872, duplicate results with --exif (and --name) [`#876`](https://github.com/RhetTbull/osxphotos/pull/876)
|
||||
- fix: dev_requirements.txt to reduce vulnerabilities [`#836`](https://github.com/RhetTbull/osxphotos/pull/836)
|
||||
- Added errors to export database, --update-errors to export, #872 [`#874`](https://github.com/RhetTbull/osxphotos/pull/874)
|
||||
- Bug fix for missing RAW images during export [`8b9af7b`](https://github.com/RhetTbull/osxphotos/commit/8b9af7be6758292b03dc291261636f334ff407a4)
|
||||
- Release files [`de584e3`](https://github.com/RhetTbull/osxphotos/commit/de584e3dec63025c583910e1fa4b247341b25379)
|
||||
- Added Ventura 13.1 to support OS versions [`830da7b`](https://github.com/RhetTbull/osxphotos/commit/830da7b3b40c1908c10310c41005fd3cf318cecd)
|
||||
|
||||
#### [v0.55.2](https://github.com/RhetTbull/osxphotos/compare/v0.55.1...v0.55.2)
|
||||
|
||||
> 13 December 2022
|
||||
|
||||
- Bug edited path bad mojave 859 [`#870`](https://github.com/RhetTbull/osxphotos/pull/870)
|
||||
- Version bump, fix for #859, wrong edited path in Mojave [`aeb6283`](https://github.com/RhetTbull/osxphotos/commit/aeb6283b2bed243be3bb3de8863cb3e40b797140)
|
||||
- Added template function example [`ee370f5`](https://github.com/RhetTbull/osxphotos/commit/ee370f5dfba78dd4f3a2835aa56e9d1bf2bc1d9a)
|
||||
- Added timewarp --function example [`2afab9e`](https://github.com/RhetTbull/osxphotos/commit/2afab9e3b16642ed4486c7a2533aeb184b6ec1a1)
|
||||
- Added edited live video path to inspect, #865 [`3c8d7e1`](https://github.com/RhetTbull/osxphotos/commit/3c8d7e13b92b8db4999e458aac2ce37eb706cc7b)
|
||||
- Updated README for supported OS versions [`c3bd04f`](https://github.com/RhetTbull/osxphotos/commit/c3bd04f257f8fbdf93034f60342943a3ffbdeb5d)
|
||||
|
||||
#### [v0.55.1](https://github.com/RhetTbull/osxphotos/compare/v0.55.0...v0.55.1)
|
||||
|
||||
> 11 December 2022
|
||||
|
||||
- Bug edited path bad mojave 859 [`#864`](https://github.com/RhetTbull/osxphotos/pull/864)
|
||||
- Version bump, fix for #859, wrong edited path in Mojave [`e4faf37`](https://github.com/RhetTbull/osxphotos/commit/e4faf3779c6c56982fba909a0efda21b86890b73)
|
||||
- Update tests.yml [`debc001`](https://github.com/RhetTbull/osxphotos/commit/debc001af9684d04a31836a6fa5705b706eb36f0)
|
||||
- Fixed edit_resource_id for Photos 5+ [`025ee36`](https://github.com/RhetTbull/osxphotos/commit/025ee36086d1515aa16a0018aaa5ae371a8a332d)
|
||||
|
||||
#### [v0.55.0](https://github.com/RhetTbull/osxphotos/compare/v0.54.4...v0.55.0)
|
||||
|
||||
> 11 December 2022
|
||||
|
||||
- Added Ventura to list of supported OS [`#863`](https://github.com/RhetTbull/osxphotos/pull/863)
|
||||
- Partial fix for #859, missing path edited on Mojave [`#862`](https://github.com/RhetTbull/osxphotos/pull/862)
|
||||
- add drodner as a contributor for bug, and userTesting [`#861`](https://github.com/RhetTbull/osxphotos/pull/861)
|
||||
- Updated build for Ventura [`327f198`](https://github.com/RhetTbull/osxphotos/commit/327f19809ee0f8883977a27eb547dcc7f9e93e11)
|
||||
- Added target architecture, #857 [`88e56bc`](https://github.com/RhetTbull/osxphotos/commit/88e56bc0b978d75b606a4adf36fa2d77ef16eb95)
|
||||
|
||||
#### [v0.54.4](https://github.com/RhetTbull/osxphotos/compare/v0.54.3...v0.54.4)
|
||||
|
||||
> 24 November 2022
|
||||
|
||||
- Added --post-function to import, #842 [`#851`](https://github.com/RhetTbull/osxphotos/pull/851)
|
||||
- Feature import parse date 847 [`#850`](https://github.com/RhetTbull/osxphotos/pull/850)
|
||||
- Version bump for release [`cad4e1e`](https://github.com/RhetTbull/osxphotos/commit/cad4e1eeff54a37826c0e08e2be1b3df3b392f94)
|
||||
- Added test for #848 [`d6fc8fc`](https://github.com/RhetTbull/osxphotos/commit/d6fc8fc3b1d276fd6b22550e50ec1bdeeb3acf6f)
|
||||
|
||||
#### [v0.54.3](https://github.com/RhetTbull/osxphotos/compare/v0.54.2...v0.54.3)
|
||||
|
||||
> 16 November 2022
|
||||
|
||||
- add zephyr325 as a contributor for bug [`#844`](https://github.com/RhetTbull/osxphotos/pull/844)
|
||||
- Version bump [`9ed1b39`](https://github.com/RhetTbull/osxphotos/commit/9ed1b394a9b2df1eca04f489c083ca3a71a7809c)
|
||||
- Fix for timewarp failure on Ventura, #841 [`40de05c`](https://github.com/RhetTbull/osxphotos/commit/40de05c5fdbc8efd8e4bd21eb8b2e17d49f4864e)
|
||||
- Updated search_info test [`f610d3c`](https://github.com/RhetTbull/osxphotos/commit/f610d3cc65a7909cfe3bd9ad4d5209f193c88a87)
|
||||
|
||||
#### [v0.54.2](https://github.com/RhetTbull/osxphotos/compare/v0.54.1...v0.54.2)
|
||||
|
||||
> 14 November 2022
|
||||
|
||||
- Added --alt-copy method for #807 [`#835`](https://github.com/RhetTbull/osxphotos/pull/835)
|
||||
- Version bump [`548071e`](https://github.com/RhetTbull/osxphotos/commit/548071e8a6f626b1f22ae7c92d209dd98bf83c27)
|
||||
- Fixed help text for , #828 [`ea76297`](https://github.com/RhetTbull/osxphotos/commit/ea76297800f3e72e6584618c126fe818f21bc1ae)
|
||||
|
||||
#### [v0.54.1](https://github.com/RhetTbull/osxphotos/compare/v0.54.0...v0.54.1)
|
||||
|
||||
> 13 November 2022
|
||||
|
||||
160
README.md
160
README.md
@@ -7,7 +7,7 @@
|
||||
[](https://pepy.tech/project/osxphotos)
|
||||
[](https://www.reddit.com/r/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.
|
||||
@@ -37,17 +37,15 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through mac
|
||||
|
||||
| macOS Version | macOS name | Photos.app version |
|
||||
| ----------------- |------------|:-------------------|
|
||||
| 13.0 | Ventura | 8.0 ? * |
|
||||
| 12.0 - 12.4 | Monterey | 7.0 ✅ ** |
|
||||
| 13.0 | Ventura | 8.0 ✅ * |
|
||||
| 12.0 - 12.6 | 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 ✅ |
|
||||
| 10.13.6 | High Sierra| 3.0 ✅ |
|
||||
| 10.12.6 | Sierra | 2.0 ✅ |
|
||||
|
||||
\* Basic functionality has been tested on a Photos library created with the developer preview of macOS Ventura (13.0). I do not have access to a Mac running Ventura beta to do further testing.
|
||||
|
||||
\*\* Some features may not be fully supported on Monterey. Notably, `--use-photokit` and `--download-missing` may or may not work depending on your configuration; this is a known issue that will be fixed if I can find a solution. Many users successfully use OSXPhotos on Monterey without problem.
|
||||
\* Some features may not be fully supported on Monterey and Ventura. Notably, `--use-photokit` and `--download-missing` may or may not work depending on your configuration; this is a known issue that will be fixed if I can find a solution. Many users successfully use OSXPhotos on Monterey without problem.
|
||||
|
||||
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.
|
||||
|
||||
@@ -64,6 +62,7 @@ If you aren't familiar with installing python applications, I recommend you inst
|
||||
* Open `Terminal` (search for `Terminal` in Spotlight or look in `Applications/Utilities`)
|
||||
* Install `homebrew` according to instructions at [https://brew.sh/](https://brew.sh/)
|
||||
* Type the following into Terminal: `brew install pipx`
|
||||
* Ensure that pipx installed packages are accessible in your PATH by typing: `pipx ensurepath`
|
||||
* Then type this: `pipx install osxphotos`
|
||||
* Now you should be able to run `osxphotos` by typing: `osxphotos`
|
||||
|
||||
@@ -653,13 +652,25 @@ osxphotos is very flexible. If you merely want to backup your Photos library, t
|
||||
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
||||
|
||||
Export photos from the Photos database. Export path DEST is required.
|
||||
|
||||
Optionally, query the Photos database using 1 or more search options; if more
|
||||
than one option is provided, they are treated as "AND" (e.g. search for photos
|
||||
matching all options). If no query options are provided, all photos will be
|
||||
exported. By default, all versions of all photos will be exported including
|
||||
edited versions, live photo movies, burst photos, and associated raw images.
|
||||
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options to
|
||||
modify this behavior.
|
||||
than one different option is provided, they are treated as "AND" (e.g. search
|
||||
for photos matching all options). If the same query option is provided
|
||||
multiple times, they are treated as "OR" (e.g. search for photos matching any
|
||||
of the options). If no query options are provided, all photos will be
|
||||
exported.
|
||||
|
||||
For example, adding the query options:
|
||||
|
||||
--person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will export all photos with either person of ("John Doe" OR "Jane Doe") AND
|
||||
keyword of "vacation"
|
||||
|
||||
By default, all versions of all photos will be exported including edited
|
||||
versions, live photo movies, burst photos, and associated raw images. See
|
||||
--skip-edited, --skip-live, --skip-bursts, and --skip-raw options to modify
|
||||
this behavior.
|
||||
|
||||
Options:
|
||||
--db PHOTOS_LIBRARY_PATH Specify Photos database path. Path to Photos
|
||||
@@ -710,7 +721,7 @@ Options:
|
||||
--no-location Search for photos with no associated location
|
||||
info (e.g. no GPS coordinates)
|
||||
--label LABEL Search for photos with image classification
|
||||
label LABEL (Photos 5 only). If more than one
|
||||
label LABEL (Photos 5+ only). If more than one
|
||||
label, treated as "OR", e.g. find photos
|
||||
matching any label
|
||||
--uti UTI Search for photos whose uniform type
|
||||
@@ -724,9 +735,9 @@ Options:
|
||||
--hidden Search for photos marked hidden.
|
||||
--not-hidden Search for photos not marked hidden.
|
||||
--shared Search for photos in shared iCloud album
|
||||
(Photos 5 only).
|
||||
(Photos 5+ only).
|
||||
--not-shared Search for photos not in shared iCloud album
|
||||
(Photos 5 only).
|
||||
(Photos 5+ only).
|
||||
--burst Search for photos that were taken in a burst.
|
||||
--not-burst Search for photos that are not part of a
|
||||
burst.
|
||||
@@ -829,6 +840,17 @@ Options:
|
||||
units. For example, the following are all
|
||||
valid and equivalent sizes: '1048576'
|
||||
'1.048576MB', '1 MiB'.
|
||||
--missing Search for photos missing from disk.
|
||||
--not-missing Search for photos present on disk (e.g. not
|
||||
missing).
|
||||
--cloudasset Search for photos that are part of an iCloud
|
||||
library
|
||||
--not-cloudasset Search for photos that are not part of an
|
||||
iCloud library
|
||||
--incloud Search for photos that are in iCloud (have
|
||||
been synched)
|
||||
--not-incloud Search for photos that are not in iCloud (have
|
||||
not been synched)
|
||||
--regex REGEX TEMPLATE Search for photos where TEMPLATE matches
|
||||
regular expression REGEX. For example, to find
|
||||
photos in an album that begins with 'Beach': '
|
||||
@@ -876,8 +898,6 @@ Options:
|
||||
evaluated. See https://github.com/RhetTbull/os
|
||||
xphotos/blob/master/examples/query_function.py
|
||||
for example of how to use this option.
|
||||
--missing Export only photos missing from the Photos
|
||||
library; must be used with --download-missing.
|
||||
--deleted Include photos from the 'Recently Deleted'
|
||||
folder.
|
||||
--deleted-only Include only photos from the 'Recently
|
||||
@@ -891,6 +911,17 @@ Options:
|
||||
would not otherwise trigger an export. See
|
||||
also --update and notes below on export and
|
||||
--update.
|
||||
--update-errors Update files that were previously exported but
|
||||
produced errors during export. For example, if
|
||||
a file produced an error with --exiftool due
|
||||
to bad metadata, this option will re-export
|
||||
the file and attempt to write the metadata
|
||||
again when used with --exiftool and --update.
|
||||
Without --update-errors, photos that were
|
||||
successfully exported but generated an error
|
||||
or warning during export will not be re-
|
||||
attempted if metadata has not changed. Must be
|
||||
used with --update.
|
||||
--ignore-signature When used with '--update', ignores file
|
||||
signature when updating files. This is useful
|
||||
if you have processed or edited exported
|
||||
@@ -2010,7 +2041,7 @@ Substitution Description
|
||||
{cr} A carriage return: '\r'
|
||||
{crlf} A carriage return + line feed: '\r\n'
|
||||
{tab} :A tab: '\t'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.54.2'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.56.2'
|
||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified
|
||||
@@ -2494,7 +2525,7 @@ The following template field substitutions are availabe for use the templating s
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|A carriage return + line feed: '\r\n'|
|
||||
|{tab}|:A tab: '\t'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.54.2'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.56.2'|
|
||||
|{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|
|
||||
@@ -2546,57 +2577,66 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/britiscurious"><img src="https://avatars1.githubusercontent.com/u/25646439?v=4?s=75" width="75px;" alt="britiscurious"/><br /><sub><b>britiscurious</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mwort"><img src="https://avatars3.githubusercontent.com/u/8170417?v=4?s=75" width="75px;" alt="Michel Wortmann"/><br /><sub><b>Michel Wortmann</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mwort" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/PabloKohan"><img src="https://avatars3.githubusercontent.com/u/8790976?v=4?s=75" width="75px;" alt="Pablo 'merKur' Kohan"/><br /><sub><b>Pablo 'merKur' Kohan</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=PabloKohan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hshore29"><img src="https://avatars2.githubusercontent.com/u/7023497?v=4?s=75" width="75px;" alt="hshore29"/><br /><sub><b>hshore29</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hshore29" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://3e.org/"><img src="https://avatars0.githubusercontent.com/u/41439?v=4?s=75" width="75px;" alt="Daniel M. Drucker"/><br /><sub><b>Daniel M. Drucker</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dmd" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Admd" title="Bug reports">🐛</a> <a href="#userTesting-dmd" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/jystervinou"><img src="https://avatars3.githubusercontent.com/u/132356?v=4?s=75" width="75px;" alt="Jean-Yves Stervinou"/><br /><sub><b>Jean-Yves Stervinou</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jystervinou" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://dethi.me/"><img src="https://avatars2.githubusercontent.com/u/1011520?v=4?s=75" width="75px;" alt="Thibault Deutsch"/><br /><sub><b>Thibault Deutsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dethi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/britiscurious"><img src="https://avatars1.githubusercontent.com/u/25646439?v=4?s=75" width="75px;" alt="britiscurious"/><br /><sub><b>britiscurious</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mwort"><img src="https://avatars3.githubusercontent.com/u/8170417?v=4?s=75" width="75px;" alt="Michel Wortmann"/><br /><sub><b>Michel Wortmann</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mwort" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PabloKohan"><img src="https://avatars3.githubusercontent.com/u/8790976?v=4?s=75" width="75px;" alt="Pablo 'merKur' Kohan"/><br /><sub><b>Pablo 'merKur' Kohan</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=PabloKohan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hshore29"><img src="https://avatars2.githubusercontent.com/u/7023497?v=4?s=75" width="75px;" alt="hshore29"/><br /><sub><b>hshore29</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hshore29" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://3e.org/"><img src="https://avatars0.githubusercontent.com/u/41439?v=4?s=75" width="75px;" alt="Daniel M. Drucker"/><br /><sub><b>Daniel M. Drucker</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dmd" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Admd" title="Bug reports">🐛</a> <a href="#userTesting-dmd" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jystervinou"><img src="https://avatars3.githubusercontent.com/u/132356?v=4?s=75" width="75px;" alt="Jean-Yves Stervinou"/><br /><sub><b>Jean-Yves Stervinou</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jystervinou" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dethi.me/"><img src="https://avatars2.githubusercontent.com/u/1011520?v=4?s=75" width="75px;" alt="Thibault Deutsch"/><br /><sub><b>Thibault Deutsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dethi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=75" width="75px;" alt="grundsch"/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=75" width="75px;" alt="Ag Primatic"/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=75" width="75px;" alt="Horst Höck"/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=75" width="75px;" alt="Jonathan Strine"/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=75" width="75px;" alt="finestream"/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=75" width="75px;" alt="Aravindo Wingeier"/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://kradalby.no"><img src="https://avatars1.githubusercontent.com/u/98431?v=4?s=75" width="75px;" alt="Kristoffer Dalby"/><br /><sub><b>Kristoffer Dalby</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=kradalby" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=75" width="75px;" alt="grundsch"/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=75" width="75px;" alt="Ag Primatic"/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=75" width="75px;" alt="Horst Höck"/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=75" width="75px;" alt="Jonathan Strine"/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=75" width="75px;" alt="finestream"/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=75" width="75px;" alt="Aravindo Wingeier"/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://kradalby.no"><img src="https://avatars1.githubusercontent.com/u/98431?v=4?s=75" width="75px;" alt="Kristoffer Dalby"/><br /><sub><b>Kristoffer Dalby</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=kradalby" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt="Rott-Apple"/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
<td align="center"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt="narensankar0529"/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt="Martin"/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt="davidjroos "/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt="Neil Pankey"/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt="Aaron van Geffen"/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt="ubrandes "/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt="Rott-Apple"/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt="narensankar0529"/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt="Martin"/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt="davidjroos "/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt="Neil Pankey"/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt="Aaron van Geffen"/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt="ubrandes "/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://blog.dewost.com/"><img src="https://avatars.githubusercontent.com/u/17090228?v=4?s=75" width="75px;" alt="Philippe Dewost"/><br /><sub><b>Philippe Dewost</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=pdewost" title="Documentation">📖</a> <a href="#example-pdewost" title="Examples">💡</a> <a href="#ideas-pdewost" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt="kaduskj"/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt="mkirkland4874"/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt="Joseph Commisso"/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||
<td align="center"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt="David Singer"/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt="oPromessa"/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a> <a href="#ideas-oPromessa" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Tests">⚠️</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="Spencer Chang"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.dewost.com/"><img src="https://avatars.githubusercontent.com/u/17090228?v=4?s=75" width="75px;" alt="Philippe Dewost"/><br /><sub><b>Philippe Dewost</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=pdewost" title="Documentation">📖</a> <a href="#example-pdewost" title="Examples">💡</a> <a href="#ideas-pdewost" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt="kaduskj"/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt="mkirkland4874"/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt="Joseph Commisso"/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt="David Singer"/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt="oPromessa"/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a> <a href="#ideas-oPromessa" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Tests">⚠️</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://spencerchang.me"><img src="https://avatars.githubusercontent.com/u/14796580?v=4?s=75" width="75px;" alt="Spencer Chang"/><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="David Gleich"/><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="Alan de Freitas"/><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="Andrew Louis"/><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="neebah"/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt="Ahti Liin"/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/xwu64"><img src="https://avatars.githubusercontent.com/u/10580396?v=4?s=75" width="75px;" alt="Xiaoliang Wu"/><br /><sub><b>Xiaoliang Wu</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=xwu64" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nullpointerninja"><img src="https://avatars.githubusercontent.com/u/62975432?v=4?s=75" width="75px;" alt="nullpointerninja"/><br /><sub><b>nullpointerninja</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anullpointerninja" title="Bug reports">🐛</a> <a href="#ideas-nullpointerninja" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.cs.purdue.edu/homes/dgleich"><img src="https://avatars.githubusercontent.com/u/33995?v=4?s=75" width="75px;" alt="David Gleich"/><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" valign="top" width="14.28%"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt="Alan de Freitas"/><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" valign="top" width="14.28%"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt="Andrew Louis"/><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" valign="top" width="14.28%"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt="neebah"/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt="Ahti Liin"/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xwu64"><img src="https://avatars.githubusercontent.com/u/10580396?v=4?s=75" width="75px;" alt="Xiaoliang Wu"/><br /><sub><b>Xiaoliang Wu</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=xwu64" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nullpointerninja"><img src="https://avatars.githubusercontent.com/u/62975432?v=4?s=75" width="75px;" alt="nullpointerninja"/><br /><sub><b>nullpointerninja</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anullpointerninja" title="Bug reports">🐛</a> <a href="#ideas-nullpointerninja" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/infused-kim"><img src="https://avatars.githubusercontent.com/u/7404004?v=4?s=75" width="75px;" alt="Kim"/><br /><sub><b>Kim</b></sub></a><br /><a href="#ideas-infused-kim" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/Se7enair"><img src="https://avatars.githubusercontent.com/u/1680106?v=4?s=75" width="75px;" alt="Christoph"/><br /><sub><b>Christoph</b></sub></a><br /><a href="#ideas-Se7enair" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="http://www.franzone.com"><img src="https://avatars.githubusercontent.com/u/900684?v=4?s=75" width="75px;" alt="franzone"/><br /><sub><b>franzone</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afranzone" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://jmuccigr.github.io/"><img src="https://avatars.githubusercontent.com/u/615115?v=4?s=75" width="75px;" alt="John Muccigrosso"/><br /><sub><b>John Muccigrosso</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajmuccigr" title="Bug reports">🐛</a> <a href="#ideas-jmuccigr" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://nomadgate.com"><img src="https://avatars.githubusercontent.com/u/1646041?v=4?s=75" width="75px;" alt="Thomas K. Running"/><br /><sub><b>Thomas K. Running</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=tkrunning" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Atkrunning" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://dalisoft.uz"><img src="https://avatars.githubusercontent.com/u/3511344?v=4?s=75" width="75px;" alt="Davlatjon Shavkatov"/><br /><sub><b>Davlatjon Shavkatov</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/infused-kim"><img src="https://avatars.githubusercontent.com/u/7404004?v=4?s=75" width="75px;" alt="Kim"/><br /><sub><b>Kim</b></sub></a><br /><a href="#ideas-infused-kim" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Se7enair"><img src="https://avatars.githubusercontent.com/u/1680106?v=4?s=75" width="75px;" alt="Christoph"/><br /><sub><b>Christoph</b></sub></a><br /><a href="#ideas-Se7enair" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.franzone.com"><img src="https://avatars.githubusercontent.com/u/900684?v=4?s=75" width="75px;" alt="franzone"/><br /><sub><b>franzone</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afranzone" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jmuccigr.github.io/"><img src="https://avatars.githubusercontent.com/u/615115?v=4?s=75" width="75px;" alt="John Muccigrosso"/><br /><sub><b>John Muccigrosso</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajmuccigr" title="Bug reports">🐛</a> <a href="#ideas-jmuccigr" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nomadgate.com"><img src="https://avatars.githubusercontent.com/u/1646041?v=4?s=75" width="75px;" alt="Thomas K. Running"/><br /><sub><b>Thomas K. Running</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=tkrunning" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Atkrunning" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://dalisoft.uz"><img src="https://avatars.githubusercontent.com/u/3511344?v=4?s=75" width="75px;" alt="Davlatjon Shavkatov"/><br /><sub><b>Davlatjon Shavkatov</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zephyr325"><img src="https://avatars.githubusercontent.com/u/5245609?v=4?s=75" width="75px;" alt="zephyr325"/><br /><sub><b>zephyr325</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Azephyr325" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drodner"><img src="https://avatars.githubusercontent.com/u/10236892?v=4?s=75" width="75px;" alt="drodner"/><br /><sub><b>drodner</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adrodner" title="Bug reports">🐛</a> <a href="#userTesting-drodner" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://fmckeogh.github.io"><img src="https://avatars.githubusercontent.com/u/8290136?v=4?s=75" width="75px;" alt="Ferdia McKeogh"/><br /><sub><b>Ferdia McKeogh</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=fmckeogh" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afmckeogh" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wellsaidlabs.com"><img src="https://avatars.githubusercontent.com/u/7424737?v=4?s=75" width="75px;" alt="Michael Petrochuk"/><br /><sub><b>Michael Petrochuk</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3APetrochukM" title="Bug reports">🐛</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=PetrochukM" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://qkeddy.github.io/quin-eddy-development-portfolio/"><img src="https://avatars.githubusercontent.com/u/9737814?v=4?s=75" width="75px;" alt="Quin Eddy"/><br /><sub><b>Quin Eddy</b></sub></a><br /><a href="#ideas-qkeddy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#data-qkeddy" title="Data">🔣</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnsturgeon"><img src="https://avatars.githubusercontent.com/u/9746310?v=4?s=75" width="75px;" alt="John Sturgeon"/><br /><sub><b>John Sturgeon</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajohnsturgeon" title="Bug reports">🐛</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=johnsturgeon" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mave2k"><img src="https://avatars.githubusercontent.com/u/8629837?v=4?s=75" width="75px;" alt="mave2k"/><br /><sub><b>mave2k</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mave2k" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -21,7 +21,7 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through mac
|
||||
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.
|
||||
|
||||
Requires python >= ``3.8``.
|
||||
Requires python >= ``3.9``.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
7
build.sh
7
build.sh
@@ -48,6 +48,9 @@ echo "Building CLI executable"
|
||||
|
||||
# zip up CLI executable
|
||||
echo "Zipping CLI executable"
|
||||
OSXPHOTOS_VERSION=$(python3 -c "import osxphotos; print(osxphotos.__version__)")
|
||||
zip dist/osxphotos_MacOS_exe_darwin_x64_v$OSXPHOTOS_VERSION.zip dist/osxphotos
|
||||
OSXPHOTOSVERSION=$(python3 -c "import osxphotos; print(osxphotos.__version__)")
|
||||
ARCHSTR=$(uname -m)
|
||||
ZIPNAME=osxphotos_MacOS_exe_darwin_${ARCHSTR}_v${OSXPHOTOSVERSION}.zip
|
||||
echo "Zipping CLI executable to $ZIPNAME"
|
||||
cd dist && zip $ZIPNAME osxphotos && cd ..
|
||||
rm dist/osxphotos
|
||||
@@ -14,4 +14,5 @@ sphinx_rtd_theme
|
||||
sphinx-copybutton
|
||||
sphinxcontrib-programoutput
|
||||
twine
|
||||
wheel
|
||||
wheel
|
||||
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
||||
@@ -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: 3598772ea20a92e6ce5c140476336b6a
|
||||
config: 2b1d245eff1c1e7c9d89e31092ea013b
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../genindex.html" /><link rel="search" title="Search" href="../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>Overview: module code - osxphotos 0.54.2 documentation</title>
|
||||
<title>Overview: module code - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="../index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos._constants - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos._constants - osxphotos 0.55.7 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.55.7 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.55.7 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -318,6 +318,8 @@
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"4"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"5"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"6"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"13"</span><span class="p">,</span> <span class="s2">"0"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"13"</span><span class="p">,</span> <span class="s2">"1"</span><span class="p">),</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="c1"># Photos 5 has persons who are empty string if unidentified face</span>
|
||||
@@ -330,6 +332,11 @@
|
||||
|
||||
<span class="c1"># Where are shared iCloud photos located?</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span> <span class="o">=</span> <span class="s2">"resources/cloudsharing/data"</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span> <span class="o">=</span> <span class="s2">"scopes/cloudsharing/data"</span>
|
||||
|
||||
<span class="c1"># Where are shared iCloud derivatives located?</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span> <span class="o">=</span> <span class="s2">"resources/cloudsharing/resources/derivatives/masters"</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span> <span class="o">=</span> <span class="s2">"scopes/cloudsharing/resources/derivatives/masters"</span>
|
||||
|
||||
<span class="c1"># What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database</span>
|
||||
<span class="n">_PHOTO_TYPE</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
@@ -433,13 +440,13 @@
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">PHOTO_NAME</span> <span class="o">=</span> <span class="mi">2056</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">SearchCategory_Photos8</span><span class="p">(</span><span class="n">SearchCategory</span><span class="p">):</span>
|
||||
<span class="sd">"""Search categories for Photos 8"""</span>
|
||||
|
||||
|
||||
<span class="c1"># Many of the category values changed in Ventura / Photos 8</span>
|
||||
<span class="c1"># and some new categories were added</span>
|
||||
<span class="n">LABEL</span> <span class="o">=</span> <span class="mi">1500</span>
|
||||
@@ -450,12 +457,12 @@
|
||||
<span class="n">KEYWORDS</span> <span class="o">=</span> <span class="mi">1200</span>
|
||||
<span class="n">TITLE</span> <span class="o">=</span> <span class="mi">1201</span>
|
||||
<span class="n">DESCRIPTION</span> <span class="o">=</span> <span class="mi">1202</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="mi">1203</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="mi">1203</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">PERSON</span> <span class="o">=</span> <span class="mi">1300</span>
|
||||
<span class="n">ACTIVITY</span> <span class="o">=</span> <span class="mi">1600</span>
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span> <span class="o">=</span> <span class="mi">2000</span>
|
||||
<span class="n">PHOTO_NAME</span> <span class="o">=</span> <span class="mi">2100</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="mi">2300</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="mi">2300</span> <span class="c1"># new in Photos 8</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">search_category_factory</span><span class="p">(</span><span class="n">version</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="n">SearchCategory</span><span class="p">:</span>
|
||||
|
||||
@@ -1,36 +1,200 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.debug - osxphotos 0.56.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.debug — osxphotos 0.47.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>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f8f8f8;
|
||||
--color-code-foreground: black;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M13 12h5" />
|
||||
<path d="M13 15h4" />
|
||||
<path d="M13 18h1" />
|
||||
<path d="M13 9h4" />
|
||||
<path d="M13 6h1" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
||||
<label class="overlay sidebar-overlay" for="__navigation">
|
||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
||||
</label>
|
||||
<label class="overlay toc-overlay" for="__toc">
|
||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.1 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.1 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></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="../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.debug</h1><div class="highlight"><pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.debug</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">"""Utilities for debugging"""</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
@@ -82,7 +246,7 @@
|
||||
|
||||
<span class="k">def</span> <span class="nf">wrap_function</span><span class="p">(</span><span class="n">function_path</span><span class="p">,</span> <span class="n">wrapper</span><span class="p">):</span>
|
||||
<span class="sd">"""Wrap a function with wrapper function"""</span>
|
||||
<span class="n">module</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="n">function_path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
<span class="n">module</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="n">function_path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"::"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">wrapt</span><span class="o">.</span><span class="n">wrap_function_wrapper</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">wrapper</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
@@ -135,72 +299,47 @@
|
||||
<span class="n">args</span><span class="p">[</span><span class="n">arg_name</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="k">return</span> <span class="n">args</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="../../index.html">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">Tutorial</a></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></li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="../../index.html">Documentation overview</a><ul>
|
||||
<li><a href="../index.html">Module code</a><ul>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2021, Rhet Turnbull
|
||||
</div>
|
||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
||||
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
<div class="icons">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
<aside class="toc-drawer no-toc">
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
|
||||
<title>osxphotos.exiftool - osxphotos 0.50.13 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.exiftool - osxphotos 0.55.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.50.13 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.55.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.50.13 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.55.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -179,7 +179,8 @@
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container"><div class="theme-toggle-container theme-toggle-content">
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
@@ -194,13 +195,13 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.exiftool</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" Yet another simple exiftool wrapper </span>
|
||||
<span></span><span class="sd">""" Yet another simple exiftool wrapper </span>
|
||||
<span class="sd"> I rolled my own for following reasons: </span>
|
||||
<span class="sd"> 1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)</span>
|
||||
<span class="sd"> 2. I wanted singleton behavior so only a single exiftool process was ever running</span>
|
||||
<span class="sd"> 3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance)</span>
|
||||
<span class="sd"> If these aren't important to you, I highly recommend you use Sven Marnach's excellent </span>
|
||||
<span class="sd"> pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """</span>
|
||||
<span class="sd"> If these aren't important to you, I highly recommend you use Sven Marnach's excellent </span>
|
||||
<span class="sd"> pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """</span>
|
||||
|
||||
|
||||
<span class="kn">import</span> <span class="nn">atexit</span>
|
||||
@@ -217,153 +218,154 @@
|
||||
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">lru_cache</span> <span class="c1"># pylint: disable=syntax-error</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"escape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"exiftool_can_write"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifTool"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifToolCaching"</span><span class="p">,</span>
|
||||
<span class="s2">"get_exiftool_path"</span><span class="p">,</span>
|
||||
<span class="s2">"terminate_exiftool"</span><span class="p">,</span>
|
||||
<span class="s2">"unescape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"escape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"exiftool_can_write"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifTool"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifToolCaching"</span><span class="p">,</span>
|
||||
<span class="s2">"get_exiftool_path"</span><span class="p">,</span>
|
||||
<span class="s2">"terminate_exiftool"</span><span class="p">,</span>
|
||||
<span class="s2">"unescape_str"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="c1"># exiftool -stay_open commands outputs this EOF marker after command is run</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="o">=</span> <span class="s2">"</span><span class="si">{ready}</span><span class="s2">"</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="o">=</span> <span class="s2">"</span><span class="si">{ready}</span><span class="s2">"</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF_LEN</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">EXIFTOOL_STAYOPEN_EOF</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># list of exiftool processes to cleanup when exiting or when terminate is called</span>
|
||||
<span class="n">EXIFTOOL_PROCESSES</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<span class="c1"># exiftool supported file types, created by utils/exiftool_supported_types.py</span>
|
||||
<span class="n">EXIFTOOL_FILETYPES_JSON</span> <span class="o">=</span> <span class="s2">"exiftool_filetypes.json"</span>
|
||||
<span class="k">with</span> <span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="n">EXIFTOOL_FILETYPES_JSON</span><span class="p">)</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="n">EXIFTOOL_FILETYPES_JSON</span> <span class="o">=</span> <span class="s2">"exiftool_filetypes.json"</span>
|
||||
<span class="k">with</span> <span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="n">EXIFTOOL_FILETYPES_JSON</span><span class="p">)</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">exiftool_can_write</span><span class="p">(</span><span class="n">suffix</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bool</span><span class="p">:</span>
|
||||
<span class="sd">"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""</span>
|
||||
<span class="sd">"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">suffix</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
<span class="n">suffix</span> <span class="o">=</span> <span class="n">suffix</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"."</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"."</span><span class="p">:</span>
|
||||
<span class="n">suffix</span> <span class="o">=</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="n">suffix</span> <span class="ow">in</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span>
|
||||
<span class="ow">and</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span><span class="p">[</span><span class="n">suffix</span><span class="p">][</span><span class="s2">"write"</span><span class="p">]</span>
|
||||
<span class="ow">and</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span><span class="p">[</span><span class="n">suffix</span><span class="p">][</span><span class="s2">"write"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">escape_str</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
|
||||
<span class="sd">"""escape string for use with exiftool -E"""</span>
|
||||
<span class="sd">"""escape string for use with exiftool -E"""</span>
|
||||
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">html</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xa;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\t</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#x9;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xd;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xa;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\t</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#x9;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xd;"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">unescape_str</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
|
||||
<span class="sd">"""unescape an HTML string returned by exiftool -E"""</span>
|
||||
<span class="sd">"""unescape an HTML string returned by exiftool -E"""</span>
|
||||
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
<span class="c1"># avoid " in values which result in json.loads() throwing an exception, #636</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"&quot;"</span><span class="p">,</span> <span class="s1">'</span><span class="se">\\</span><span class="s1">"'</span><span class="p">)</span>
|
||||
<span class="c1"># avoid " in values which result in json.loads() throwing an exception, #636</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"&quot;"</span><span class="p">,</span> <span class="s1">'</span><span class="se">\\</span><span class="s1">"'</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">html</span><span class="o">.</span><span class="n">unescape</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="nd">@atexit</span><span class="o">.</span><span class="n">register</span>
|
||||
<span class="k">def</span> <span class="nf">terminate_exiftool</span><span class="p">():</span>
|
||||
<span class="sd">"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""</span>
|
||||
<span class="sd">"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""</span>
|
||||
<span class="k">for</span> <span class="n">proc</span> <span class="ow">in</span> <span class="n">EXIFTOOL_PROCESSES</span><span class="p">:</span>
|
||||
<span class="n">proc</span><span class="o">.</span><span class="n">_stop_proc</span><span class="p">()</span>
|
||||
|
||||
|
||||
<span class="nd">@lru_cache</span><span class="p">(</span><span class="n">maxsize</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
|
||||
<span class="k">def</span> <span class="nf">get_exiftool_path</span><span class="p">():</span>
|
||||
<span class="sd">"""return path of exiftool, cache result"""</span>
|
||||
<span class="k">if</span> <span class="n">exiftool_path</span> <span class="o">:=</span> <span class="n">shutil</span><span class="o">.</span><span class="n">which</span><span class="p">(</span><span class="s2">"exiftool"</span><span class="p">):</span>
|
||||
<span class="sd">"""return path of exiftool, cache result"""</span>
|
||||
<span class="k">if</span> <span class="n">exiftool_path</span> <span class="o">:=</span> <span class="n">shutil</span><span class="o">.</span><span class="n">which</span><span class="p">(</span><span class="s2">"exiftool"</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">exiftool_path</span><span class="o">.</span><span class="n">rstrip</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span>
|
||||
<span class="s2">"Could not find exiftool. Please download and install from "</span>
|
||||
<span class="s2">"https://exiftool.org/"</span>
|
||||
<span class="s2">"Could not find exiftool. Please download and install from "</span>
|
||||
<span class="s2">"https://exiftool.org/"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">_ExifToolProc</span><span class="p">:</span>
|
||||
<span class="sd">"""Runs exiftool in a subprocess via Popen</span>
|
||||
<span class="sd"> Creates a singleton object"""</span>
|
||||
<span class="sd">"""Runs exiftool in a subprocess via Popen</span>
|
||||
<span class="sd"> Creates a singleton object"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="s2">"instance"</span><span class="p">)</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="s2">"instance"</span><span class="p">)</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
|
||||
<span class="bp">cls</span><span class="o">.</span><span class="n">instance</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">large_file_support</span><span class="o">=</span><span class="kc">True</span><span class="p">):</span>
|
||||
<span class="sd">"""construct _ExifToolProc singleton object or return instance of already created object</span>
|
||||
<span class="sd">"""construct _ExifToolProc singleton object or return instance of already created object</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> exiftool: optional path to exiftool binary (if not provided, will search path to find it)</span>
|
||||
<span class="sd"> large_file_support: if True, enables large file support (>4GB) via `-api largefilesupport=1`</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process_running"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process_running"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="c1"># already running</span>
|
||||
<span class="k">if</span> <span class="n">exiftool</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">exiftool</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"exiftool subprocess already running, "</span>
|
||||
<span class="sa">f</span><span class="s2">"ignoring exiftool=</span><span class="si">{</span><span class="n">exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="sa">f</span><span class="s2">"exiftool subprocess already running, "</span>
|
||||
<span class="sa">f</span><span class="s2">"ignoring exiftool=</span><span class="si">{</span><span class="n">exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_large_file_support</span> <span class="o">=</span> <span class="n">large_file_support</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span> <span class="o">=</span> <span class="n">exiftool</span> <span class="ow">or</span> <span class="n">get_exiftool_path</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">(</span><span class="n">large_file_support</span><span class="o">=</span><span class="n">large_file_support</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return the exiftool subprocess"""</span>
|
||||
<span class="sd">"""return the exiftool subprocess"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">(</span><span class="n">large_file_support</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_large_file_support</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">pid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">pid</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">exiftool</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path to exiftool process"""</span>
|
||||
<span class="sd">"""return path to exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_start_proc</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">large_file_support</span><span class="p">):</span>
|
||||
<span class="sd">"""start exiftool in batch mode"""</span>
|
||||
<span class="sd">"""start exiftool in batch mode"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"exiftool already running: </span><span class="si">{self._process}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"exiftool already running: </span><span class="si">{self._process}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span>
|
||||
|
||||
<span class="c1"># open exiftool process</span>
|
||||
<span class="c1"># make sure /usr/bin at start of path so exiftool can find xattr (see #636)</span>
|
||||
<span class="n">env</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
<span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'/usr/bin/:</span><span class="si">{</span><span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span><span class="si">}</span><span class="s1">'</span>
|
||||
<span class="n">large_file_args</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-api"</span><span class="p">,</span> <span class="s2">"largefilesupport=1"</span><span class="p">]</span> <span class="k">if</span> <span class="n">large_file_support</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
<span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'/usr/bin/:</span><span class="si">{</span><span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span><span class="si">}</span><span class="s1">'</span>
|
||||
<span class="n">large_file_args</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-api"</span><span class="p">,</span> <span class="s2">"largefilesupport=1"</span><span class="p">]</span> <span class="k">if</span> <span class="n">large_file_support</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span>
|
||||
<span class="p">[</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span><span class="p">,</span>
|
||||
<span class="s2">"-stay_open"</span><span class="p">,</span> <span class="c1"># keep process open in batch mode</span>
|
||||
<span class="s2">"True"</span><span class="p">,</span> <span class="c1"># -stay_open=True, keep process open in batch mode</span>
|
||||
<span class="s2">"-stay_open"</span><span class="p">,</span> <span class="c1"># keep process open in batch mode</span>
|
||||
<span class="s2">"True"</span><span class="p">,</span> <span class="c1"># -stay_open=True, keep process open in batch mode</span>
|
||||
<span class="o">*</span><span class="n">large_file_args</span><span class="p">,</span>
|
||||
<span class="s2">"-@"</span><span class="p">,</span> <span class="c1"># read command-line arguments from file</span>
|
||||
<span class="s2">"-"</span><span class="p">,</span> <span class="c1"># read from stdin</span>
|
||||
<span class="s2">"-common_args"</span><span class="p">,</span> <span class="c1"># specifies args common to all commands subsequently run</span>
|
||||
<span class="s2">"-n"</span><span class="p">,</span> <span class="c1"># no print conversion (e.g. print tag values in machine readable format)</span>
|
||||
<span class="s2">"-P"</span><span class="p">,</span> <span class="c1"># Preserve file modification date/time</span>
|
||||
<span class="s2">"-G"</span><span class="p">,</span> <span class="c1"># print group name for each tag</span>
|
||||
<span class="s2">"-E"</span><span class="p">,</span> <span class="c1"># escape tag values for HTML (allows use of HTML &#xa; for newlines)</span>
|
||||
<span class="s2">"-@"</span><span class="p">,</span> <span class="c1"># read command-line arguments from file</span>
|
||||
<span class="s2">"-"</span><span class="p">,</span> <span class="c1"># read from stdin</span>
|
||||
<span class="s2">"-common_args"</span><span class="p">,</span> <span class="c1"># specifies args common to all commands subsequently run</span>
|
||||
<span class="s2">"-n"</span><span class="p">,</span> <span class="c1"># no print conversion (e.g. print tag values in machine readable format)</span>
|
||||
<span class="s2">"-P"</span><span class="p">,</span> <span class="c1"># Preserve file modification date/time</span>
|
||||
<span class="s2">"-G"</span><span class="p">,</span> <span class="c1"># print group name for each tag</span>
|
||||
<span class="s2">"-E"</span><span class="p">,</span> <span class="c1"># escape tag values for HTML (allows use of HTML &#xa; for newlines)</span>
|
||||
<span class="p">],</span>
|
||||
<span class="n">stdin</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
|
||||
<span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
|
||||
@@ -375,14 +377,14 @@
|
||||
<span class="n">EXIFTOOL_PROCESSES</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_stop_proc</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""stop the exiftool process if it's running, otherwise, do nothing"""</span>
|
||||
<span class="sd">"""stop the exiftool process if it's running, otherwise, do nothing"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="k">return</span>
|
||||
|
||||
<span class="k">with</span> <span class="n">contextlib</span><span class="o">.</span><span class="n">suppress</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"-stay_open</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"False</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"-stay_open</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"False</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">flush</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">_process</span><span class="o">.</span><span class="n">communicate</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
|
||||
@@ -395,7 +397,7 @@
|
||||
|
||||
|
||||
<div class="viewcode-block" id="ExifTool"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool">[docs]</a><span class="k">class</span> <span class="nc">ExifTool</span><span class="p">:</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags"""</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
@@ -405,7 +407,7 @@
|
||||
<span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">large_file_support</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""Create ExifTool object</span>
|
||||
<span class="sd">"""Create ExifTool object</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> file: path to image file</span>
|
||||
@@ -416,7 +418,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ExifTool instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">file</span> <span class="o">=</span> <span class="n">filepath</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="o">=</span> <span class="n">overwrite</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">flags</span> <span class="o">=</span> <span class="n">flags</span> <span class="ow">or</span> <span class="p">[]</span>
|
||||
@@ -435,7 +437,7 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">process</span>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.setvalue"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.setvalue">[docs]</a> <span class="k">def</span> <span class="nf">setvalue</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="sd">"""Set tag to value(s); if value is None, will delete tag</span>
|
||||
<span class="sd">"""Set tag to value(s); if value is None, will delete tag</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag: str; name of tag to set</span>
|
||||
@@ -447,27 +449,27 @@
|
||||
<span class="sd"> If error generated by exiftool, returns False and sets self.error to error string</span>
|
||||
<span class="sd"> If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string</span>
|
||||
<span class="sd"> If called in context manager, returns True (execution is delayed until exiting context manager)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="s2">""</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="s2">""</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">escape_str</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span>
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># avoid "Warning: Some character(s) could not be encoded in Latin" warning</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-iptc:codedcharacterset=utf8"</span><span class="p">)</span>
|
||||
<span class="c1"># avoid "Warning: Some character(s) could not be encoded in Latin" warning</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-iptc:codedcharacterset=utf8"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_commands</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">True</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.addvalues"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.addvalues">[docs]</a> <span class="k">def</span> <span class="nf">addvalues</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="o">*</span><span class="n">values</span><span class="p">):</span>
|
||||
<span class="sd">"""Add one or more value(s) to tag</span>
|
||||
<span class="sd">"""Add one or more value(s) to tag</span>
|
||||
<span class="sd"> If more than one value is passed, each value will be added to the tag</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
@@ -485,32 +487,32 @@
|
||||
<span class="sd"> the values being added are not already in the EXIF data</span>
|
||||
<span class="sd"> For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,</span>
|
||||
<span class="sd"> but for others, such as EXIF:ISO, this will literally add a value to the existing value.</span>
|
||||
<span class="sd"> It's up to the caller to know what exiftool will do for each tag</span>
|
||||
<span class="sd"> It's up to the caller to know what exiftool will do for each tag</span>
|
||||
<span class="sd"> If setvalue called before addvalues, exiftool does not appear to add duplicates,</span>
|
||||
<span class="sd"> but if addvalues called without first calling setvalue, exiftool will add duplicate values</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Must pass at least one value"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Must pass at least one value"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Can't add None value to tag"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Can't add None value to tag"</span><span class="p">)</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">escape_str</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">+=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">+=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_commands</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">True</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.run_commands"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.run_commands">[docs]</a> <span class="k">def</span> <span class="nf">run_commands</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""Run commands in the exiftool process and return result.</span>
|
||||
<span class="sd">"""Run commands in the exiftool process and return result.</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> *commands: exiftool commands to run</span>
|
||||
@@ -524,35 +526,35 @@
|
||||
<span class="sd"> error: if exiftool generated errors, string containing otherwise empty string</span>
|
||||
|
||||
<span class="sd"> Note: Also sets self.warning and self.error if warning or error generated.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"exiftool process is not running"</span><span class="p">)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"exiftool process is not running"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">commands</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"must provide one or more command to run"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"must provide one or more command to run"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">commands</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">commands</span><span class="p">)</span>
|
||||
<span class="n">commands</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">commands</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">if</span> <span class="n">no_file</span> <span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">fsencode</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="p">)</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">if</span> <span class="n">no_file</span> <span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">fsencode</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">flags</span><span class="p">:</span>
|
||||
<span class="c1"># need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]</span>
|
||||
<span class="c1"># need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]</span>
|
||||
<span class="n">flags</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">flags</span><span class="p">:</span>
|
||||
<span class="n">flags</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">split</span><span class="p">())</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">f</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">flags</span><span class="p">])</span>
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">f</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">flags</span><span class="p">])</span>
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="p">(</span>
|
||||
<span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">c</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">commands</span><span class="p">])</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">c</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">commands</span><span class="p">])</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="n">filename</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"-execute</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"-execute</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># send the command</span>
|
||||
@@ -560,19 +562,19 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
|
||||
|
||||
<span class="c1"># read the output</span>
|
||||
<span class="n">output</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">output</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="k">while</span> <span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="ow">not</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">output</span><span class="p">):</span>
|
||||
<span class="n">line</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">readline</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Warning"</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Warning"</span><span class="p">):</span>
|
||||
<span class="n">warning</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">elif</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Error"</span><span class="p">):</span>
|
||||
<span class="k">elif</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Error"</span><span class="p">):</span>
|
||||
<span class="n">error</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">output</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">warning</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">warning</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">error</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">error</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">warning</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">warning</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">error</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">error</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">warning</span> <span class="o">=</span> <span class="n">warning</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">=</span> <span class="n">error</span>
|
||||
|
||||
@@ -580,41 +582,41 @@
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">pid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">pid</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">version</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns exiftool version"""</span>
|
||||
<span class="n">ver</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-ver"</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">ver</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="sd">"""returns exiftool version"""</span>
|
||||
<span class="n">ver</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-ver"</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">ver</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag_groups</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd"> returns empty dict if no tags</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> normalized: if True, dict keys are all normalized to lower case (default is False)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">json_str</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">json_str</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">json_str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="nb">dict</span><span class="p">()</span>
|
||||
<span class="n">json_str</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json_str</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="n">json_str</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json_str</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">json_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="c1"># will fail with some commands, e.g --ext AVI which produces</span>
|
||||
<span class="c1"># 'No file with specified extension' instead of json</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"error loading json returned by exiftool: </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">json_str</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="c1"># 'No file with specified extension' instead of json</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"error loading json returned by exiftool: </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">json_str</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="nb">dict</span><span class="p">()</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exifdict</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
|
||||
<span class="c1"># strip tag groups</span>
|
||||
<span class="n">exif_new</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">exifdict</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="n">k</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">".*:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
|
||||
<span class="n">k</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">".*:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
|
||||
<span class="n">exif_new</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">v</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exif_new</span>
|
||||
|
||||
@@ -624,17 +626,17 @@
|
||||
<span class="k">return</span> <span class="n">exifdict</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.json"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns JSON string containing all EXIF tags and values from exiftool"""</span>
|
||||
<span class="n">json</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="n">json</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="sd">"""returns JSON string containing all EXIF tags and values from exiftool"""</span>
|
||||
<span class="n">json</span><span class="p">,</span> <span class="n">_</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">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="n">json</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="n">json</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_read_exif</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""read exif data from file"""</span>
|
||||
<span class="sd">"""read exif data from file"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"file: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="si">}</span><span class="se">\n</span><span class="s2">exiftool: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">_exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"file: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="si">}</span><span class="se">\n</span><span class="s2">exiftool: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">_exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
@@ -650,15 +652,15 @@
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">ExifToolCaching</span><span class="p">(</span><span class="n">ExifTool</span><span class="p">):</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags, with caching.</span>
|
||||
<span class="sd"> Use this only when you know the file's EXIF data will not be changed by any external process.</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags, with caching.</span>
|
||||
<span class="sd"> Use this only when you know the file's EXIF data will not be changed by any external process.</span>
|
||||
|
||||
<span class="sd"> Creates a singleton cached ExifTool instance"""</span>
|
||||
<span class="sd"> Creates a singleton cached ExifTool instance"""</span>
|
||||
|
||||
<span class="n">_singletons</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="n">filepath</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">:</span>
|
||||
<span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">[</span><span class="n">filepath</span><span class="p">]</span> <span class="o">=</span> <span class="n">_ExifToolCaching</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="n">exiftool</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">[</span><span class="n">filepath</span><span class="p">]</span>
|
||||
@@ -666,7 +668,7 @@
|
||||
|
||||
<span class="k">class</span> <span class="nc">_ExifToolCaching</span><span class="p">(</span><span class="n">ExifTool</span><span class="p">):</span>
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""Create read-only ExifTool object that caches values</span>
|
||||
<span class="sd">"""Create read-only ExifTool object that caches values</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> file: path to image file</span>
|
||||
@@ -674,21 +676,21 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ExifTool instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="n">exiftool</span><span class="p">,</span> <span class="n">overwrite</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">run_commands</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">commands</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"-json"</span><span class="p">,</span> <span class="s2">"-ver"</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">commands</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"-json"</span><span class="p">,</span> <span class="s2">"-ver"</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="n">no_file</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">setvalue</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">value</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">addvalues</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="o">*</span><span class="n">values</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</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">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span><span class="p">:</span>
|
||||
@@ -696,13 +698,13 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag_groups</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd"> returns empty dict if no tags</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> normalized: if True, dict keys are all normalized to lower case (default is False)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</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">_asdict_cache</span><span class="p">[</span><span class="n">tag_groups</span><span class="p">][</span><span class="n">normalized</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
@@ -714,7 +716,7 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span><span class="p">[</span><span class="n">tag_groups</span><span class="p">][</span><span class="n">normalized</span><span class="p">]</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">flush_cache</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Clear cached data so that calls to json or asdict return fresh data"""</span>
|
||||
<span class="sd">"""Clear cached data so that calls to json or asdict return fresh data"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
</pre></div>
|
||||
@@ -754,7 +756,9 @@
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.photoinfo - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.photoinfo - osxphotos 0.56.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.1 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.1 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -201,6 +201,8 @@
|
||||
<span class="sd">PhotosDB.photos() returns a list of PhotoInfo objects</span>
|
||||
<span class="sd">"""</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">contextlib</span>
|
||||
<span class="kn">import</span> <span class="nn">dataclasses</span>
|
||||
<span class="kn">import</span> <span class="nn">datetime</span>
|
||||
@@ -231,8 +233,11 @@
|
||||
<span class="n">_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_PROJECT_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="n">BURST_DEFAULT_PICK</span><span class="p">,</span>
|
||||
<span class="n">BURST_KEY</span><span class="p">,</span>
|
||||
<span class="n">BURST_NOT_SELECTED</span><span class="p">,</span>
|
||||
@@ -344,38 +349,59 @@
|
||||
<span class="k">return</span> <span class="n">photopath</span> <span class="c1"># path would be meaningless until downloaded</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_4</span><span class="p">()</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"shared"</span><span class="p">]:</span>
|
||||
<span class="c1"># shared photo</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</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="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">]</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"/"</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_4</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</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="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_5</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns candidate path for original photo on Photos >= version 5"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"shared"</span><span class="p">]:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_5_shared</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">])</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">]</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
|
||||
<span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_5_shared</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns candidate path for shared photo on Photos >= version 5"""</span>
|
||||
<span class="c1"># shared library path differs on Photos 5-7, Photos 8+</span>
|
||||
<span class="n">shared_path</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">>=</span> <span class="mi">8</span>
|
||||
<span class="k">else</span> <span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">shared_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># a shared video has two files, the poster image and the video</span>
|
||||
<span class="c1"># the poster (image frame shown in Photos) is named UUID.poster.JPG</span>
|
||||
<span class="c1"># the video file is named UUID.medium.MP4</span>
|
||||
<span class="c1"># this method returns the path to the video file</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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">.medium.MP4"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">shared_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="n">filename</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path for photo on Photos <= version 4"""</span>
|
||||
<span class="sd">"""Returns candidate path for original photo on Photos <= version 4"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"has_raw"</span><span class="p">]:</span>
|
||||
<span class="c1"># return the path to JPEG even if RAW is original</span>
|
||||
<span class="n">vol</span> <span class="o">=</span> <span class="p">(</span>
|
||||
@@ -400,9 +426,6 @@
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"imagePath"</span><span class="p">]</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="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
@@ -414,14 +437,20 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4</span><span class="p">()</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4</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">_path_edited</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5</span><span class="p">()</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5</span><span class="p">()</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> 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="bp">self</span><span class="o">.</span><span class="n">_path_edited</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited for Photos >= 5"""</span>
|
||||
<span class="sd">"""Returns candidate path_edited for Photos >= 5 or None if cannot be determined"""</span>
|
||||
<span class="c1"># In Photos 5.0 / Catalina / MacOS 10.15:</span>
|
||||
<span class="c1"># edited photos appear to always be converted to .jpeg and stored in</span>
|
||||
<span class="c1"># library_name/resources/renders/X/UUID_1_201_a.jpeg</span>
|
||||
@@ -456,95 +485,123 @@
|
||||
<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">"WARNING: unknown type </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s1">'type'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"renders"</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"renders"</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">filename</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="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_predicted_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return predicted path_edited for Photos <= 4"""</span>
|
||||
<span class="n">edit_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">"edit_resource_id_photo"</span><span class="p">]</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="k">if</span> <span class="n">uti_edited</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_edited</span><span class="p">:</span>
|
||||
<span class="n">ext</span> <span class="o">=</span> <span class="n">get_preferred_uti_extension</span><span class="p">(</span><span class="n">uti_edited</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">ext</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.</span><span class="si">{</span><span class="n">ext</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># if we get here, we couldn't figure out the extension</span>
|
||||
<span class="c1"># so try to figure out the type and build the filename</span>
|
||||
<span class="n">type_</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">"type"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">type_</span> <span class="o">==</span> <span class="n">_PHOTO_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a photo</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.jpeg"</span>
|
||||
<span class="k">elif</span> <span class="n">type_</span> <span class="o">==</span> <span class="n">_MOVIE_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a movie</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unknown type </span><span class="si">{</span><span class="n">type_</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return path_edited for Photos <= 4; modified version of code in PhotoInfo to debug #859"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"hasAdjustments"</span><span class="p">]:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">edit_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">"edit_resource_id"</span><span class="p">]:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_predicted_path_edited_4</span><span class="p">()</span>
|
||||
<span class="k">except</span> <span class="ne">ValueError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"ERROR: </span><span class="si">{</span><span class="n">e</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">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># the heuristic failed, so try to find the file</span>
|
||||
<span class="n">rootdir</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="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</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="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
|
||||
<span class="c1"># check again to see if we found a valid file</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> 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="c1"># TODO: might be possible for original/master to be missing but edit to still be there</span>
|
||||
<span class="c1"># if self._info["isMissing"] == 1:</span>
|
||||
<span class="c1"># photopath = None # path would be meaningless until downloaded</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited for Photos <= 4"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">></span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Wrong database format!"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"hasAdjustments"</span><span class="p">]:</span>
|
||||
<span class="n">edit_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">"edit_resource_id"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">edit_id</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># todo: is this always true or do we need to search file file_id under folder_id</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTO_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a photo</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.jpeg"</span>
|
||||
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="n">_MOVIE_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a movie</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># don't know what it is!</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">"WARNING: unknown type </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s1">'type'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># photopath appears to usually be in "00" subfolder but</span>
|
||||
<span class="c1"># could be elsewhere--I haven't figured out this logic yet</span>
|
||||
<span class="c1"># first see if it's in 00</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="s2">"00"</span><span class="p">,</span> <span class="n">filename</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="n">rootdir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
|
||||
<span class="c1"># check again to see if we found a valid file</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="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: edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> 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">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"</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"> hasAdjustments but edit_resource_id is None"</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> 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">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</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"> hasAdjustments but edit_resource_id is None"</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>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">path_edited_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path to edited version of live photo movie; only valid for Photos 5+"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="sd">"""return path to edited version of live photo movie"""</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">_path_edited_live_photo</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5_live_photo</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4_live_photo</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">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5_live_photo</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_predicted_path_edited_live_photo_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return predicted path_edited for Photos <= 4"""</span>
|
||||
<span class="c1"># need the resource id for the video, not the photo (edit_resource_id is for photo)</span>
|
||||
<span class="k">if</span> <span class="n">edit_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">"edit_resource_id_video"</span><span class="p">]:</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"videocomplementoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited_live_photo for Photos <= 4"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">></span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Wrong database format!"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_predicted_path_edited_live_photo_4</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># the heuristic failed, so try to find the file</span>
|
||||
<span class="n">rootdir</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="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</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="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">next</span><span class="p">(</span>
|
||||
<span class="p">(</span>
|
||||
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span>
|
||||
<span class="p">),</span>
|
||||
<span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">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: edited live photo file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_5_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited_live_photo for Photos >= 5"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
@@ -604,15 +661,11 @@
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">filepath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">])</span>
|
||||
|
||||
<span class="c1"># raw files have same name as original but with _4.raw_ext appended</span>
|
||||
<span class="c1"># I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4</span>
|
||||
<span class="c1"># see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc</span>
|
||||
<span class="n">raw_file</span> <span class="o">=</span> <span class="n">list_directory</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">startswith</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filestem</span><span class="si">}</span><span class="s2">_4"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">raw_file</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">if</span> <span class="n">raw_file</span> <span class="o">:=</span> <span class="n">list_directory</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">startswith</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filestem</span><span class="si">}</span><span class="s2">_4"</span><span class="p">):</span>
|
||||
<span class="n">photopath</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="n">filepath</span><span class="p">)</span> <span class="o">/</span> <span class="n">raw_file</span><span class="p">[</span><span class="mi">0</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="n">photopath</span><span class="o">.</span><span class="n">is_file</span><span class="p">()</span> <span class="k">else</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"># is a reference</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
@@ -962,8 +1015,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">_photos_ver</span> <span class="o"><</span> <span class="mi">7</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"UTI_raw"</span><span class="p">]</span>
|
||||
|
||||
<span class="n">rawpath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span>
|
||||
<span class="k">if</span> <span class="n">rawpath</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">rawpath</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">get_uti_for_extension</span><span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">rawpath</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
@@ -1060,7 +1112,7 @@
|
||||
<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>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">live_model_id</span><span class="p">)</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">live_model_id</span><span class="p">)</span>
|
||||
<span class="n">library_path</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">library_path</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library_path</span><span class="p">,</span>
|
||||
@@ -1068,7 +1120,7 @@
|
||||
<span class="s2">"media"</span><span class="p">,</span>
|
||||
<span class="s2">"master"</span><span class="p">,</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span>
|
||||
<span class="s2">"00"</span><span class="p">,</span>
|
||||
<span class="n">nn_id</span><span class="p">,</span>
|
||||
<span class="sa">f</span><span class="s2">"jpegvideocomplement_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span><span class="p">,</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>
|
||||
@@ -1131,17 +1183,13 @@
|
||||
<span class="n">modelid</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">"modelID"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">modelid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">modelid</span><span class="p">)</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">modelid</span><span class="p">)</span>
|
||||
<span class="n">derivatives_root</span> <span class="o">=</span> <span class="p">(</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">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
|
||||
<span class="o">/</span> <span class="sa">f</span><span class="s2">"resources/proxies/derivatives/</span><span class="si">{</span><span class="n">folder_id</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># photos appears to usually be in "00" subfolder but</span>
|
||||
<span class="c1"># could be elsewhere--I haven't figured out this logic yet</span>
|
||||
<span class="c1"># first see if it's in 00</span>
|
||||
|
||||
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="s2">"00"</span> <span class="o">/</span> <span class="n">file_id</span>
|
||||
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="n">nn_id</span> <span class="o">/</span> <span class="n">file_id</span>
|
||||
<span class="k">if</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">is_dir</span><span class="p">():</span>
|
||||
<span class="n">files</span> <span class="o">=</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">"*"</span><span class="p">)</span>
|
||||
<span class="n">files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span><span class="p">)</span>
|
||||
@@ -1163,14 +1211,17 @@
|
||||
<span class="sd">"""Return paths to all derivative (preview) files for shared iCloud photos in Photos >= 5"""</span>
|
||||
<span class="n">directory</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># first char of uuid</span>
|
||||
<span class="c1"># only 1 derivative for shared photos and it's called 'UUID_4_5005_c.jpeg'</span>
|
||||
<span class="n">derivative_path</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">>=</span> <span class="mi">8</span>
|
||||
<span class="k">else</span> <span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">derivative_path</span> <span class="o">=</span> <span class="p">(</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">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
|
||||
<span class="o">/</span> <span class="s2">"resources/cloudsharing/resources/derivatives/masters"</span>
|
||||
<span class="o">/</span> <span class="n">derivative_path</span>
|
||||
<span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/</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">_4_5005_c.jpeg"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">derivative_path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
|
||||
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">derivative_path</span><span class="p">)]</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">derivative_path</span><span class="p">)]</span> <span class="k">if</span> <span class="n">derivative_path</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">panorama</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
@@ -1597,6 +1648,11 @@
|
||||
<span class="n">metadata</span> <span class="o">=</span> <span class="n">plistlib</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
|
||||
<span class="k">return</span> <span class="n">metadata</span>
|
||||
|
||||
<span class="nd">@cached_property</span>
|
||||
<span class="k">def</span> <span class="nf">fingerprint</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="sd">"""Returns fingerprint of original photo as a string"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"masterFingerprint"</span><span class="p">]</span>
|
||||
|
||||
<div class="viewcode-block" id="PhotoInfo.detected_text"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.detected_text">[docs]</a> <span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
|
||||
<span class="sd">"""Detects text in photo and returns lists of results as (detected text, confidence)</span>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../../genindex.html" /><link rel="search" title="Search" href="../../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.photosdb.photosdb - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.photosdb.photosdb - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../../index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -1029,10 +1029,10 @@
|
||||
<span class="c1"># for compatability with Photos 5 where album kind is ZKIND</span>
|
||||
<span class="s2">"kind"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"customsortkey"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"customsortkey"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="c1"># get details about folders</span>
|
||||
@@ -1150,7 +1150,8 @@
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid,</span>
|
||||
<span class="sd"> RKMaster.fingerprint</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1182,7 +1183,8 @@
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid,</span>
|
||||
<span class="sd"> RKMaster.fingerprint</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1233,6 +1235,7 @@
|
||||
<span class="c1"># 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)</span>
|
||||
<span class="c1"># 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)</span>
|
||||
<span class="c1"># 44 RKMaster.importGroupUuid -- to get date added from RKImportGroup</span>
|
||||
<span class="c1"># 45 RKMaster.fingerprint -- fingerprint / hash of the file</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1430,6 +1433,9 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">44</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"fok_import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># fingerprint</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"masterFingerprint"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">45</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># photos 5+ only, for shared photos</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudownerhashedpersonid"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
@@ -1494,7 +1500,9 @@
|
||||
<span class="sd"> RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,</span>
|
||||
<span class="sd"> RKModelResource.attachedModelType, RKModelResource.resourceType</span>
|
||||
<span class="sd"> FROM RKVersion</span>
|
||||
<span class="sd"> JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """</span>
|
||||
<span class="sd"> JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId</span>
|
||||
<span class="sd"> ORDER BY RKModelResource.modelId</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># Order of results:</span>
|
||||
@@ -1504,8 +1512,8 @@
|
||||
<span class="c1"># 3 RKModelResource.resourceTag</span>
|
||||
<span class="c1"># 4 RKModelResource.UTI</span>
|
||||
<span class="c1"># 5 RKVersion.specialType</span>
|
||||
<span class="c1"># 6 RKModelResource.attachedModelType</span>
|
||||
<span class="c1"># 7 RKModelResource.resourceType</span>
|
||||
<span class="c1"># 6 RKModelResource.attachedModelType (2 = edit)</span>
|
||||
<span class="c1"># 7 RKModelResource.resourceType (4 = photo, 8 = video)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1517,18 +1525,30 @@
|
||||
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">"UNADJUSTED"</span>
|
||||
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">==</span> <span class="mi">2</span>
|
||||
<span class="p">):</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="n">is_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">"WARNING: found more than one edit_resource_id for "</span>
|
||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="c1"># TODO: I think there should never be more than one edit but</span>
|
||||
<span class="c1"># I've seen this once in my library</span>
|
||||
<span class="c1"># should we return all edits or just most recent one?</span>
|
||||
<span class="c1"># For now, return most recent edit</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</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="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited"</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">resource_type</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="c1"># UTI_edited will be set to the appropriate UTI for the edited resource below</span>
|
||||
<span class="c1"># a live photo that's edited will have both a photo and video resource but the photo</span>
|
||||
<span class="c1"># UTI will be used for the edited live photo, see #859</span>
|
||||
<span class="k">if</span> <span class="n">resource_type</span> <span class="o">==</span> <span class="mi">4</span><span class="p">:</span>
|
||||
<span class="c1"># photo</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_photo"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="n">is_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">"WARNING: found more than one edit_resource_id_photo for "</span>
|
||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</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="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</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="k">elif</span> <span class="n">resource_type</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="c1"># video</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_video"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="n">is_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">"WARNING: found more than one edit_resource_id_video for "</span>
|
||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</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="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</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="c1"># get details on external edits</span>
|
||||
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
@@ -1581,9 +1601,27 @@
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># init any uuids that had no edits or live photos</span>
|
||||
<span class="c1"># also initialized UTI_edited and edit_resource_id</span>
|
||||
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_photo"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_video"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"UTI_edited_photo"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"UTI_edited_video"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="c1"># UTI_edited will be set to the appropriate UTI for the edited resource below</span>
|
||||
<span class="c1"># a live photo that's edited will have both a photo and video resource but the photo</span>
|
||||
<span class="c1"># UTI will be used for the edited live photo</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="s2">"live_model_id"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"live_model_id"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"modeResourceIsOnDisk"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -1989,7 +2027,8 @@
|
||||
<span class="s2">"parentfolder"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="s2">"pk"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span>
|
||||
<span class="s2">"intrash"</span><span class="p">:</span> <span class="kc">False</span> <span class="k">if</span> <span class="n">album</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">True</span><span class="p">,</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span> <span class="c1"># iPhone Photos.sqlite can have null value</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="mi">0</span><span class="p">,</span> <span class="c1"># iPhone Photos.sqlite can have null value</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">13</span><span class="p">],</span>
|
||||
@@ -2366,6 +2405,12 @@
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"alt_master_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"raw_info"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
|
||||
<span class="c1"># Photos 4 only</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">info</span>
|
||||
|
||||
<span class="c1"># compute signatures for finding possible duplicates</span>
|
||||
@@ -3549,7 +3594,7 @@
|
||||
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span> <span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">photo_list</span><span class="p">))</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">min_size</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span>
|
||||
@@ -3656,7 +3701,7 @@
|
||||
<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="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">matching_photos</span><span class="p">))</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">added_after</span><span class="p">:</span>
|
||||
<span class="n">added_after</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">added_after</span>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.phototemplate - osxphotos 0.54.1 documentation</title>
|
||||
<!-- Generated with Sphinx 5.3.0 and Furo 2022.12.07 -->
|
||||
<title>osxphotos.phototemplate - osxphotos 0.55.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=91d0f0d1c444bdcb17a68e833c7a53903343c195" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.55.4 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,10 +146,10 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.55.4 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
@@ -1912,9 +1912,7 @@
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
<div class="icons">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,39 +1,203 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.scoreinfo - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.scoreinfo — osxphotos 0.47.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>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f8f8f8;
|
||||
--color-code-foreground: black;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M13 12h5" />
|
||||
<path d="M13 15h4" />
|
||||
<path d="M13 18h1" />
|
||||
<path d="M13 9h4" />
|
||||
<path d="M13 6h1" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
||||
<label class="overlay sidebar-overlay" for="__navigation">
|
||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
||||
</label>
|
||||
<label class="overlay toc-overlay" for="__toc">
|
||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></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="../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.scoreinfo</h1><div class="highlight"><pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.scoreinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" ScoreInfo class to expose computed score info from the library """</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
|
||||
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span><span class="p">,</span> <span class="n">asdict</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
|
||||
|
||||
@@ -70,74 +234,53 @@
|
||||
<span class="n">tastefully_blurred</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_chosen_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_framed_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_timed_shot</span><span class="p">:</span> <span class="nb">float</span></div>
|
||||
</pre></div>
|
||||
<span class="n">well_timed_shot</span><span class="p">:</span> <span class="nb">float</span>
|
||||
|
||||
</div>
|
||||
<div class="viewcode-block" id="ScoreInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.ScoreInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return ScoreInfo as a dict"""</span>
|
||||
<span class="k">return</span> <span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span></div></div>
|
||||
</pre></div>
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="../../index.html">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">Tutorial</a></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></li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="../../index.html">Documentation overview</a><ul>
|
||||
<li><a href="../index.html">Module code</a><ul>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2021, Rhet Turnbull
|
||||
</div>
|
||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
||||
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
<div class="icons">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
<aside class="toc-drawer no-toc">
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -357,7 +357,7 @@ Template Substitutions
|
||||
* - {tab}
|
||||
- :A tab: '\t'
|
||||
* - {osxphotos_version}
|
||||
- The osxphotos version, e.g. '0.54.2'
|
||||
- The osxphotos version, e.g. '0.56.2'
|
||||
* - {osxphotos_cmd_line}
|
||||
- The full command line used to run osxphotos
|
||||
* - {album}
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
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.54.2',
|
||||
VERSION: '0.56.2',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
868
docs/cli.html
868
docs/cli.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos" href="overview.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos 0.54.2 documentation</title>
|
||||
<title>osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="#"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -258,6 +258,7 @@
|
||||
<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-run">run</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-snap">snap</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-sync">sync</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-theme">theme</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-timewarp">timewarp</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
|
||||
@@ -361,6 +362,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.timeout"><code class="docutils literal notranslate"><span class="pre">ExportOptions.timeout</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.touch_file"><code class="docutils literal notranslate"><span class="pre">ExportOptions.touch_file</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.update"><code class="docutils literal notranslate"><span class="pre">ExportOptions.update</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.update_errors"><code class="docutils literal notranslate"><span class="pre">ExportOptions.update_errors</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_albums_as_keywords"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_albums_as_keywords</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_persons_as_keywords"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_persons_as_keywords</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_photos_export"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_photos_export</span></code></a></li>
|
||||
@@ -457,6 +459,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.face_info"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.face_info</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.favorite"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.favorite</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.filename"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.filename</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.fingerprint"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.fingerprint</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.has_raw"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.has_raw</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.hasadjustments"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.hasadjustments</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.hdr"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.hdr</span></code></a></li>
|
||||
@@ -656,7 +659,10 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.year"><code class="docutils literal notranslate"><span class="pre">QueryOptions.year</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo"><code class="docutils literal notranslate"><span class="pre">ScoreInfo</span></code></a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo"><code class="docutils literal notranslate"><span class="pre">ScoreInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo.asdict"><code class="docutils literal notranslate"><span class="pre">ScoreInfo.asdict()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.SearchInfo"><code class="docutils literal notranslate"><span class="pre">SearchInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.activities"><code class="docutils literal notranslate"><span class="pre">SearchInfo.activities</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.all"><code class="docutils literal notranslate"><span class="pre">SearchInfo.all</span></code></a></li>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Tutorial" href="tutorial.html" /><link rel="prev" title="Welcome to OSXPhotos’s documentation!" href="index.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos - osxphotos 0.54.2 documentation</title>
|
||||
<title>OSXPhotos - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos python API" href="reference.html" /><link rel="prev" title="OSXPhotos Template System" href="template_help.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Python Package Overview - osxphotos 0.54.2 documentation</title>
|
||||
<title>OSXPhotos Python Package Overview - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Python Module Index - osxphotos 0.54.2 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Python Module Index - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -122,7 +122,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -145,7 +145,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Search - osxphotos 0.54.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Search - osxphotos 0.56.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
@@ -121,7 +121,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Python Package Overview" href="package_overview.html" /><link rel="prev" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Template System - osxphotos 0.54.2 documentation</title>
|
||||
<title>OSXPhotos Template System - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -608,7 +608,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{osxphotos_version}</p></td>
|
||||
<td><p>The osxphotos version, e.g. ‘0.54.2’</p></td>
|
||||
<td><p>The osxphotos version, e.g. ‘0.56.2’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{osxphotos_cmd_line}</p></td>
|
||||
<td><p>The full command line used to run osxphotos</p></td>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" /><link rel="prev" title="OSXPhotos" href="overview.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Tutorial - osxphotos 0.54.2 documentation</title>
|
||||
<title>OSXPhotos Tutorial - osxphotos 0.56.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.56.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.2 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
|
||||
@@ -357,7 +357,7 @@ Template Substitutions
|
||||
* - {tab}
|
||||
- :A tab: '\t'
|
||||
* - {osxphotos_version}
|
||||
- The osxphotos version, e.g. '0.54.2'
|
||||
- The osxphotos version, e.g. '0.56.2'
|
||||
* - {osxphotos_cmd_line}
|
||||
- The full command line used to run osxphotos
|
||||
* - {album}
|
||||
|
||||
151
examples/find_bad_extensions.py
Normal file
151
examples/find_bad_extensions.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||
|
||||
This can be run with osxphotos via: osxphotos run find_bad_extensions.py
|
||||
|
||||
For help, run: osxphotos run find_bad_extensions.py --help
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import click
|
||||
from rich import print
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
from osxphotos.cli.common import get_data_dir
|
||||
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
|
||||
def check_extension(filepath: str) -> tuple[bool, str, str]:
|
||||
"""Check if file extension is correct for image file using exiftool
|
||||
|
||||
Args:
|
||||
filepath: path to file to check
|
||||
|
||||
Returns: tuple of (bool, str, str) where bool is True if extension is correct, False if not
|
||||
and str, str is the current extension, correct extension or current extension if correct
|
||||
"""
|
||||
filepath = pathlib.Path(filepath)
|
||||
current_ext = filepath.suffix.lower()
|
||||
|
||||
current_ext = current_ext[1:] if current_ext else "" # remove leading dot
|
||||
exiftool = ExifTool(filepath)
|
||||
correct_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||
if current_ext != correct_ext:
|
||||
# there are some extensions that have more than one valid extension
|
||||
# there are likely more but these are the ones I've seen so far
|
||||
is_correct = (
|
||||
current_ext in ("jpg", "jpeg") and correct_ext in ("jpg", "jpeg")
|
||||
) or (current_ext in ("tif", "tiff") and correct_ext in ("tif", "tiff"))
|
||||
else:
|
||||
is_correct = True
|
||||
return is_correct, current_ext, correct_ext
|
||||
|
||||
|
||||
def check_photo(
|
||||
photo: PhotoInfo, recheck: bool, version: str, kvstore: SQLiteKVStore
|
||||
) -> None:
|
||||
"""Check PhotoInfo for correct extension
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance
|
||||
recheck: if True, recheck even if previously checked
|
||||
version: "original" or "edited"
|
||||
kvstore: SQLiteKVStore instance to store results
|
||||
"""
|
||||
photo_path = photo.path if version == "original" else photo.path_edited
|
||||
if photo_path is None:
|
||||
print(
|
||||
f":warning-emoji: [yellow]No {version} path for photo: {photo.original_filename} ({photo.uuid})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
if recheck or f"{photo.uuid}:{version}" not in kvstore:
|
||||
is_correct, current_ext, correct_ext = check_extension(photo_path)
|
||||
if not is_correct:
|
||||
print(
|
||||
f"{photo.original_filename} ({version}) has incorrect extension: [red]{current_ext}[/] should be [green]{correct_ext}[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# output results as CSV to stdout
|
||||
csv.writer(sys.stdout).writerow(
|
||||
[
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
version,
|
||||
current_ext,
|
||||
correct_ext,
|
||||
photo_path,
|
||||
]
|
||||
)
|
||||
kvstore[f"{photo.uuid}:{version}"] = (is_correct, current_ext, correct_ext)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--library",
|
||||
default=None,
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
||||
help="Path to Photos library to use. Default is to use default Photos library.",
|
||||
)
|
||||
@click.option(
|
||||
"--recheck",
|
||||
is_flag=True,
|
||||
help="Recheck all files even if previously checked and cached.",
|
||||
)
|
||||
@click.option(
|
||||
"--edited",
|
||||
is_flag=True,
|
||||
help="Check edited versions of photos in addition to originals.",
|
||||
)
|
||||
def main(library: str, recheck: bool, edited: bool):
|
||||
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||
|
||||
This can be run with osxphotos via: `osxphotos run find_bad_extensions.py`
|
||||
|
||||
Both STDOUT and STDERR are used to output results.
|
||||
|
||||
STDOUT is used to output a CSV file with the following columns:
|
||||
|
||||
uuid, original_filename, version, current_extension, correct_extension, path
|
||||
|
||||
Thus, to save the results to a file, run:
|
||||
|
||||
osxphotos run find_bad_extensions.py > results.csv
|
||||
"""
|
||||
|
||||
# exiftool required to run
|
||||
try:
|
||||
get_exiftool_path()
|
||||
except FileNotFoundError as e:
|
||||
print(
|
||||
":cross_mark-emoji: [red]Could not find exiftool. Please download and install"
|
||||
" from https://exiftool.org/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise click.Abort() from e
|
||||
|
||||
# path to the cache database to store results of extension check
|
||||
cache_db_path = os.path.join(get_data_dir(), "bad_extensions.db")
|
||||
kvstore = SQLiteKVStore(
|
||||
cache_db_path, wal=True, serialize=json.dumps, deserialize=json.loads
|
||||
)
|
||||
kvstore.about = "osxphotos bad extensions cache"
|
||||
print(f"Using cache database: [blue]{cache_db_path}", file=sys.stderr)
|
||||
|
||||
# load the Photos database and check each photo
|
||||
photosdb = PhotosDB(dbfile=library)
|
||||
for photo in photosdb.photos():
|
||||
check_photo(photo, recheck, "original", kvstore)
|
||||
if edited and photo.hasadjustments:
|
||||
check_photo(photo, recheck, "edited", kvstore)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
examples/fix_export_extension.py
Normal file
44
examples/fix_export_extension.py
Normal file
@@ -0,0 +1,44 @@
|
||||
""" Example function for use with osxphotos export --post-function option """
|
||||
|
||||
import pathlib
|
||||
from typing import Callable
|
||||
|
||||
from osxphotos import ExportResults, PhotoInfo
|
||||
from osxphotos.exiftool import ExifTool
|
||||
|
||||
|
||||
def fix_extension(
|
||||
photo: PhotoInfo, results: ExportResults, verbose: Callable, **kwargs
|
||||
):
|
||||
"""Call this with osxphotos export /path/to/export --post-function fix_export_extension.py::fix_extension
|
||||
This will get called immediately after the photo has been exported
|
||||
|
||||
See full example here: https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance for the photo that's just been exported
|
||||
results: ExportResults instance with information about the files associated with the exported photo
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
Will not be called if --dry-run flag is enabled
|
||||
Will be called immediately after export and before any --post-command commands are executed
|
||||
"""
|
||||
|
||||
for filepath in results.exported:
|
||||
filepath = pathlib.Path(filepath)
|
||||
ext = filepath.suffix.lower()
|
||||
if not ext:
|
||||
continue
|
||||
ext = ext[1:] # remove leading dot
|
||||
exiftool = ExifTool(filepath)
|
||||
actual_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||
if ext != actual_ext and (ext not in ("jpg", "jpeg") or actual_ext != "jpg"):
|
||||
# WARNING: Does not check for name collisions; left as an exercise for the reader
|
||||
verbose(f"Fixing extension for {filepath} from {ext} to {actual_ext}")
|
||||
new_filepath = filepath.with_suffix(f".{actual_ext}")
|
||||
verbose(f"Renaming {filepath} to {new_filepath}")
|
||||
filepath.rename(new_filepath)
|
||||
28
examples/post_function_import.py
Normal file
28
examples/post_function_import.py
Normal file
@@ -0,0 +1,28 @@
|
||||
""" Example function for use with osxphotos import --post-function option """
|
||||
|
||||
import typing as t
|
||||
import photoscript
|
||||
import pathlib
|
||||
|
||||
def post_function(
|
||||
photo: photoscript.Photo, filepath: pathlib.Path, verbose: t.Callable, **kwargs
|
||||
):
|
||||
"""Call this with osxphotos import /file/to/import --post-function post_function.py::post_function
|
||||
This will get called immediately after the photo has been imported into Photos
|
||||
and all metadata been set (e.g. --exiftool, --title, etc.)
|
||||
|
||||
Args:
|
||||
photo: photoscript.Photo instance for the photo that's just been imported
|
||||
filepath: pathlib.Path to the file that was imported (this is the path to the source file, not the path inside the Photos library)
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
See https://rhettbull.github.io/PhotoScript/ for documentation on photoscript
|
||||
"""
|
||||
|
||||
# add a note to the photo's description
|
||||
verbose("Adding note to description")
|
||||
photo.description = f"{photo.description} (imported with osxphotos)"
|
||||
38
examples/simple_example.py
Normal file
38
examples/simple_example.py
Normal file
@@ -0,0 +1,38 @@
|
||||
""" Simple usage of the package """
|
||||
import os.path
|
||||
|
||||
import osxphotos
|
||||
|
||||
def main():
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.albums)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
print(photosdb.albums_as_dict)
|
||||
|
||||
# find all photos with Keyword = Foo and containing John Smith
|
||||
photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"])
|
||||
|
||||
# find all photos that include Alice Smith but do not contain the keyword Bar
|
||||
photos = [p for p in photosdb.photos(persons=["Alice Smith"])
|
||||
if p not in photosdb.photos(keywords=["Bar"]) ]
|
||||
for p in photos:
|
||||
print(
|
||||
p.uuid,
|
||||
p.filename,
|
||||
p.original_filename,
|
||||
p.date,
|
||||
p.description,
|
||||
p.title,
|
||||
p.keywords,
|
||||
p.albums,
|
||||
p.persons,
|
||||
p.path,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
84
examples/template_function_expected_path.py
Normal file
84
examples/template_function_expected_path.py
Normal file
@@ -0,0 +1,84 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template
|
||||
Returns expected path for a missing photos
|
||||
Use: osxphotos query --missing --field original_path "{function:photopath.py::original}"
|
||||
or for edited photos: osxphotos query --missing --field edited_path "{function:photopath.py::edited}"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from osxphotos import ExportOptions, PhotoInfo
|
||||
from osxphotos._constants import _MOVIE_TYPE, _PHOTO_TYPE, _PHOTOS_5_SHARED_PHOTO_PATH
|
||||
|
||||
|
||||
def original(
|
||||
photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs
|
||||
) -> Union[list[str], str]:
|
||||
"""returns expected path for original photo or None if path cannot be determined
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: osxphotos.ExportOptions object
|
||||
args: optional str of arguments passed to template function
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
if photo._info["shared"]:
|
||||
# shared photo
|
||||
return os.path.join(
|
||||
photo._db._library_path,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
photo._info["directory"],
|
||||
photo._info["filename"],
|
||||
)
|
||||
elif photo._info["directory"].startswith("/"):
|
||||
# referenced photo
|
||||
return os.path.join(photo._info["directory"], photo._info["filename"])
|
||||
else:
|
||||
# regular photo
|
||||
return os.path.join(
|
||||
photo._db._masters_path,
|
||||
photo._info["directory"],
|
||||
photo._info["filename"],
|
||||
)
|
||||
|
||||
|
||||
def edited(
|
||||
photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs
|
||||
) -> Union[list[str], str]:
|
||||
"""returns expected path for edited photo or None if path cannot be determined
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: osxphotos.ExportOptions object
|
||||
args: optional str of arguments passed to template function
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
if not photo._info["hasAdjustments"]:
|
||||
return []
|
||||
|
||||
library = photo._db._library_path
|
||||
directory = photo._uuid[0] # first char of uuid
|
||||
filename = None
|
||||
if photo._info["type"] == _PHOTO_TYPE:
|
||||
# it's a photo
|
||||
if photo._db._photos_ver != 5 and photo.uti == "public.heic":
|
||||
filename = f"{photo._uuid}_1_201_a.heic"
|
||||
else:
|
||||
filename = f"{photo._uuid}_1_201_a.jpeg"
|
||||
elif photo._info["type"] == _MOVIE_TYPE:
|
||||
# it's a movie
|
||||
filename = f"{photo._uuid}_2_0_a.mov"
|
||||
else:
|
||||
return []
|
||||
|
||||
return os.path.join(library, "resources", "renders", directory, filename)
|
||||
39
examples/timewarp_filename.py
Normal file
39
examples/timewarp_filename.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Example function for use with `osxphotos timewarp --function`
|
||||
|
||||
Call this as: `osxphotos timewarp --function timewarp_filename.py::parse_date_time_from_filename`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable
|
||||
|
||||
from photoscript import Photo
|
||||
from strpdatetime import strpdatetime
|
||||
|
||||
|
||||
def parse_date_time_from_filename(
|
||||
photo: Photo, path: str | None, tz_sec: int, tz_name: str, verbose: Callable
|
||||
) -> tuple[datetime, int]:
|
||||
"""Function for use with `osxphotos timewarp --function` that parses date/time from filename in format "YYYY-MM-DD FILENAME.jpg"
|
||||
|
||||
Args:
|
||||
photo: Photo object
|
||||
path: path to photo, which may be None if photo is not on disk
|
||||
tz_sec: timezone offset from UTC in seconds
|
||||
tz_name: timezone name
|
||||
verbose: function to print verbose messages
|
||||
|
||||
Returns:
|
||||
tuple of (new date/time as datetime.datetime, and new timezone offset from UTC in seconds as int)
|
||||
"""
|
||||
filename = photo.filename
|
||||
try:
|
||||
datetime = strpdatetime(filename, "^%Y-%m-%d")
|
||||
except ValueError:
|
||||
verbose(f"Unable to parse date/time from {filename}")
|
||||
return photo.date, tz_sec
|
||||
|
||||
verbose(f"Updating {photo.filename} date/time: {datetime}")
|
||||
|
||||
return datetime, tz_sec
|
||||
@@ -24,11 +24,32 @@ datas.extend(
|
||||
]
|
||||
)
|
||||
|
||||
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
||||
package_imports = [
|
||||
["photoscript", ["photoscript.applescript"]],
|
||||
]
|
||||
for package, files in package_imports:
|
||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||
datas.extend((os.path.join(proot, f), package) for f in files)
|
||||
|
||||
# Add attribute data files for osxmetadata
|
||||
# There is probably a better way to do this but this works
|
||||
proot = os.path.dirname(importlib.import_module("osxmetadata").__file__)
|
||||
for attribute_data in [
|
||||
"audio_attributes.json",
|
||||
"common_attributes.json",
|
||||
"filesystem_attributes.json",
|
||||
"image_attributes.json",
|
||||
"mdimporter_constants.json",
|
||||
"nsurl_resource_keys.json",
|
||||
"video_attributes.json",
|
||||
]:
|
||||
datas.append(
|
||||
(
|
||||
os.path.join(proot, "attribute_data", attribute_data),
|
||||
"osxmetadata/attribute_data",
|
||||
)
|
||||
)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
@@ -63,4 +84,5 @@ exe = EXE(
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
target_architecture="universal2",
|
||||
)
|
||||
|
||||
@@ -121,6 +121,8 @@ _TESTED_OS_VERSIONS = [
|
||||
("12", "4"),
|
||||
("12", "5"),
|
||||
("12", "6"),
|
||||
("13", "0"),
|
||||
("13", "1"),
|
||||
]
|
||||
|
||||
# Photos 5 has persons who are empty string if unidentified face
|
||||
@@ -133,6 +135,11 @@ _EXIF_TOOL_URL = "https://exiftool.org/"
|
||||
|
||||
# Where are shared iCloud photos located?
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
|
||||
_PHOTOS_8_SHARED_PHOTO_PATH = "scopes/cloudsharing/data"
|
||||
|
||||
# Where are shared iCloud derivatives located?
|
||||
_PHOTOS_5_SHARED_DERIVATIVE_PATH = "resources/cloudsharing/resources/derivatives/masters"
|
||||
_PHOTOS_8_SHARED_DERIVATIVE_PATH = "scopes/cloudsharing/resources/derivatives/masters"
|
||||
|
||||
# What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database
|
||||
_PHOTO_TYPE = 0
|
||||
@@ -236,13 +243,13 @@ class SearchCategory:
|
||||
PHOTO_TYPE_FAVORITES,
|
||||
]
|
||||
PHOTO_NAME = 2056
|
||||
CAMERA = None # Photos 8+ only
|
||||
DETECTED_TEXT = None # Photos 8+ only
|
||||
CAMERA = None # Photos 8+ only
|
||||
DETECTED_TEXT = None # Photos 8+ only
|
||||
|
||||
|
||||
class SearchCategory_Photos8(SearchCategory):
|
||||
"""Search categories for Photos 8"""
|
||||
|
||||
|
||||
# Many of the category values changed in Ventura / Photos 8
|
||||
# and some new categories were added
|
||||
LABEL = 1500
|
||||
@@ -253,12 +260,12 @@ class SearchCategory_Photos8(SearchCategory):
|
||||
KEYWORDS = 1200
|
||||
TITLE = 1201
|
||||
DESCRIPTION = 1202
|
||||
DETECTED_TEXT = 1203 # new in Photos 8
|
||||
DETECTED_TEXT = 1203 # new in Photos 8
|
||||
PERSON = 1300
|
||||
ACTIVITY = 1600
|
||||
PHOTO_TYPE_FAVORITES = 2000
|
||||
PHOTO_NAME = 2100
|
||||
CAMERA = 2300 # new in Photos 8
|
||||
CAMERA = 2300 # new in Photos 8
|
||||
|
||||
|
||||
def search_category_factory(version: int) -> SearchCategory:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.54.2"
|
||||
__version__ = "0.56.2"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""cli package for osxphotos"""
|
||||
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
@@ -26,13 +27,13 @@ for func_name in args.get("--watch", []):
|
||||
wrap_function(func_name, debug_watch)
|
||||
print(f"Watching {func_name}")
|
||||
except AttributeError:
|
||||
print(f"{func_name} does not exist")
|
||||
print(f"{func_name} does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for func_name in args.get("--breakpoint", []):
|
||||
try:
|
||||
wrap_function(func_name, debug_breakpoint)
|
||||
print(f"Breakpoint added for {func_name}")
|
||||
print(f"Breakpoint added for {func_name}", file=sys.stderr)
|
||||
except AttributeError:
|
||||
print(f"{func_name} does not exist")
|
||||
sys.exit(1)
|
||||
@@ -40,7 +41,7 @@ for func_name in args.get("--breakpoint", []):
|
||||
args = get_debug_flags(["--debug"], sys.argv)
|
||||
if args.get("--debug", False):
|
||||
set_debug(True)
|
||||
print("Debugging enabled")
|
||||
print("Debugging enabled", file=sys.stderr)
|
||||
|
||||
from .about import about
|
||||
from .albums import albums
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""Command line interface for osxphotos """
|
||||
|
||||
import atexit
|
||||
import cProfile
|
||||
import io
|
||||
import pstats
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos._constants import PROFILE_SORT_KEYS
|
||||
from osxphotos._version import __version__
|
||||
|
||||
from .about import about
|
||||
@@ -28,11 +34,13 @@ from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
from .version import version
|
||||
from .common import DEBUG_OPTIONS
|
||||
|
||||
|
||||
# Click CLI object & context settings
|
||||
@@ -47,20 +55,51 @@ CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@click.group(context_settings=CTX_SETTINGS)
|
||||
@click.version_option(__version__, "--version", "-v")
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@DEBUG_OPTIONS
|
||||
@click.option(
|
||||
"--debug",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
help="Enable debug output",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
"--profile", is_flag=True, hidden=OSXPHOTOS_HIDDEN, help="Enable profiling"
|
||||
)
|
||||
@click.option(
|
||||
"--profile-sort",
|
||||
default=None,
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
multiple=True,
|
||||
metavar="SORT_KEY",
|
||||
type=click.Choice(
|
||||
PROFILE_SORT_KEYS,
|
||||
case_sensitive=True,
|
||||
),
|
||||
help="Sort profiler output by SORT_KEY as specified at https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats. "
|
||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@click.version_option(__version__, "--version", "-v")
|
||||
@click.pass_context
|
||||
def cli_main(ctx, db, json_, debug):
|
||||
"""osxphotos: query and export your Photos library"""
|
||||
def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
|
||||
"""osxphotos: the multi-tool for your Photos library"""
|
||||
# Note: kwargs is used to catch any debug options passed in
|
||||
# the debug options are handled in cli/__init__.py
|
||||
# before this function is called
|
||||
ctx.obj = CLI_Obj(db=db, json=json_, group=cli_main)
|
||||
if profile:
|
||||
click.echo("Profiling...")
|
||||
profile_sort = profile_sort or ["cumulative"]
|
||||
click.echo(f"Profile sort_stats order: {profile_sort}")
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
def at_exit():
|
||||
pr.disable()
|
||||
click.echo("Profiling completed")
|
||||
s = io.StringIO()
|
||||
pstats.Stats(pr, stream=s).strip_dirs().sort_stats(
|
||||
*profile_sort
|
||||
).print_stats()
|
||||
click.echo(s.getvalue())
|
||||
|
||||
atexit.register(at_exit)
|
||||
|
||||
|
||||
# install CLI commands
|
||||
@@ -90,6 +129,7 @@ for command in [
|
||||
repl,
|
||||
run,
|
||||
snap,
|
||||
sync,
|
||||
theme,
|
||||
timewarp,
|
||||
tutorial,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Globals and constants use by the CLI commands"""
|
||||
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
@@ -10,6 +11,7 @@ from packaging import version
|
||||
from xdg import xdg_config_home, xdg_data_home
|
||||
|
||||
import osxphotos
|
||||
from osxphotos import QueryOptions
|
||||
from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.utils import get_latest_version
|
||||
@@ -42,10 +44,17 @@ __all__ = [
|
||||
"get_photos_db",
|
||||
"load_uuid_from_file",
|
||||
"noop",
|
||||
"query_options_from_kwargs",
|
||||
"time_stamp",
|
||||
]
|
||||
|
||||
|
||||
class IncompatibleQueryOptions(Exception):
|
||||
"""Incompatible query options"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
@@ -263,7 +272,7 @@ def QUERY_OPTIONS(f):
|
||||
"--label",
|
||||
metavar="LABEL",
|
||||
multiple=True,
|
||||
help="Search for photos with image classification label LABEL (Photos 5 only). "
|
||||
help="Search for photos with image classification label LABEL (Photos 5+ only). "
|
||||
'If more than one label, treated as "OR", e.g. find photos matching any label',
|
||||
),
|
||||
o(
|
||||
@@ -296,12 +305,12 @@ def QUERY_OPTIONS(f):
|
||||
o(
|
||||
"--shared",
|
||||
is_flag=True,
|
||||
help="Search for photos in shared iCloud album (Photos 5 only).",
|
||||
help="Search for photos in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
o(
|
||||
"--not-shared",
|
||||
is_flag=True,
|
||||
help="Search for photos not in shared iCloud album (Photos 5 only).",
|
||||
help="Search for photos not in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
o(
|
||||
"--burst",
|
||||
@@ -476,6 +485,32 @@ def QUERY_OPTIONS(f):
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
o("--missing", is_flag=True, help="Search for photos missing from disk."),
|
||||
o(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
),
|
||||
o(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
),
|
||||
o(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
),
|
||||
o(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
),
|
||||
o(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
),
|
||||
o(
|
||||
"--regex",
|
||||
metavar="REGEX TEMPLATE",
|
||||
@@ -544,18 +579,24 @@ def DEBUG_OPTIONS(f):
|
||||
),
|
||||
o(
|
||||
"--watch",
|
||||
metavar="FUNCTION_PATH",
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Watch function calls. For example, to watch all calls to FileUtil.copy: "
|
||||
"'--watch osxphotos.fileutil.FileUtil.copy'. More than one --watch option can be specified.",
|
||||
help="Watch function or method calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to watch all calls to FileUtil.copy() which is in "
|
||||
"osxphotos.fileutil, use: "
|
||||
"'--watch osxphotos.fileutil::FileUtil.copy'. More than one --watch option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
o(
|
||||
"--breakpoint",
|
||||
metavar="FUNCTION_PATH",
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Add breakpoint to function calls. For example, to add breakpoint to FileUtil.copy: "
|
||||
"'--breakpoint osxphotos.fileutil.FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
help="Add breakpoint to function calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to set a breakpoint for calls to "
|
||||
"FileUtil.copy() which is in osxphotos.fileutil, use: "
|
||||
"'--breakpoint osxphotos.fileutil::FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
]
|
||||
@@ -628,3 +669,103 @@ def check_version():
|
||||
"to suppress this message and prevent osxphotos from checking for latest version.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
|
||||
def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
"""Validate query options and create a QueryOptions instance"""
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
"added_after",
|
||||
"added_before",
|
||||
"added_in_last",
|
||||
"album",
|
||||
"duplicate",
|
||||
"edited",
|
||||
"exif",
|
||||
"external_edit",
|
||||
"folder",
|
||||
"from_date",
|
||||
"from_time",
|
||||
"has_raw",
|
||||
"keyword",
|
||||
"label",
|
||||
"max_size",
|
||||
"min_size",
|
||||
"name",
|
||||
"person",
|
||||
"query_eval",
|
||||
"query_function",
|
||||
"regex",
|
||||
"selected",
|
||||
"to_date",
|
||||
"to_time",
|
||||
"uti",
|
||||
"uuid_from_file",
|
||||
"uuid",
|
||||
"year",
|
||||
]
|
||||
exclusive = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("favorite", "not_favorite"),
|
||||
("has_comment", "no_comment"),
|
||||
("has_likes", "no_likes"),
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("keyword", "no_keyword"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("portrait", "not_portrait"),
|
||||
("screenshot", "not_screenshot"),
|
||||
("selfie", "not_selfie"),
|
||||
("shared", "not_shared"),
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("is_reference", "not_reference"),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
|
||||
[
|
||||
all([any(kwargs["title"]), kwargs["no_title"]]),
|
||||
all([any(kwargs["description"]), kwargs["no_description"]]),
|
||||
all([any(kwargs["place"]), kwargs["no_place"]]),
|
||||
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
|
||||
]
|
||||
):
|
||||
raise IncompatibleQueryOptions
|
||||
|
||||
# can also be used with --deleted/--not-deleted which are not part of
|
||||
# standard query options
|
||||
try:
|
||||
if kwargs["deleted"] and kwargs["not_deleted"]:
|
||||
raise IncompatibleQueryOptions
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# actually have something to query
|
||||
include_photos = True
|
||||
include_movies = True # default searches for everything
|
||||
if kwargs["only_movies"]:
|
||||
include_photos = False
|
||||
if kwargs["only_photos"]:
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
if kwargs["uuid_from_file"]:
|
||||
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
|
||||
query_dict = {field: kwargs.get(field) for field in query_fields}
|
||||
query_dict["photos"] = include_photos
|
||||
query_dict["movies"] = include_movies
|
||||
query_dict["uuid"] = uuid
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
@@ -28,7 +28,7 @@ from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .export import export, render_and_validate_report
|
||||
from .param_types import ExportDBType, TemplateString
|
||||
from .report_writer import ReportWriterNoOp, report_writer_factory
|
||||
from .report_writer import ReportWriterNoOp, export_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
@@ -320,7 +320,7 @@ def process_files(
|
||||
report = render_and_validate_report(
|
||||
options.report, options.exiftool_path, export_dir
|
||||
)
|
||||
report_writer = report_writer_factory(report, options.append)
|
||||
report_writer = export_report_writer_factory(report, options.append)
|
||||
else:
|
||||
report_writer = ReportWriterNoOp()
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
"""export command for osxphotos CLI"""
|
||||
|
||||
import atexit
|
||||
import cProfile
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import pstats
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -34,7 +29,6 @@ from osxphotos._constants import (
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||
OSXPHOTOS_EXPORT_DB,
|
||||
POST_COMMAND_CATEGORIES,
|
||||
PROFILE_SORT_KEYS,
|
||||
SIDECAR_EXIFTOOL,
|
||||
SIDECAR_JSON,
|
||||
SIDECAR_XMP,
|
||||
@@ -47,7 +41,7 @@ from osxphotos.configoptions import (
|
||||
)
|
||||
from osxphotos.crash_reporter import crash_reporter, set_crash_data
|
||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||
from osxphotos.debug import is_debug, set_debug
|
||||
from osxphotos.debug import is_debug
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
||||
from osxphotos.fileutil import FileUtil, FileUtilNoOp, FileUtilShUtil
|
||||
@@ -78,7 +72,6 @@ from .common import (
|
||||
CLI_COLOR_WARNING,
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DEBUG_OPTIONS,
|
||||
DELETED_OPTIONS,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
@@ -92,7 +85,7 @@ from .common import (
|
||||
from .help import ExportCommand, get_help_msg
|
||||
from .list import _list_libraries
|
||||
from .param_types import ExportDBType, FunctionCall, TemplateString
|
||||
from .report_writer import ReportWriterNoOp, report_writer_factory
|
||||
from .report_writer import ReportWriterNoOp, export_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
|
||||
@@ -105,11 +98,6 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
"--no-progress", is_flag=True, help="Do not display progress bar during export."
|
||||
)
|
||||
@QUERY_OPTIONS
|
||||
@click.option(
|
||||
"--missing",
|
||||
is_flag=True,
|
||||
help="Export only photos missing from the Photos library; must be used with --download-missing.",
|
||||
)
|
||||
@DELETED_OPTIONS
|
||||
@click.option(
|
||||
"--update",
|
||||
@@ -124,6 +112,17 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
"if their metadata has changed even if this would not otherwise trigger an export. "
|
||||
"See also --update and notes below on export and --update.",
|
||||
)
|
||||
@click.option(
|
||||
"--update-errors",
|
||||
is_flag=True,
|
||||
help="Update files that were previously exported but produced errors during export. "
|
||||
"For example, if a file produced an error with --exiftool due to bad metadata, "
|
||||
"this option will re-export the file and attempt to write the metadata again "
|
||||
"when used with --exiftool and --update. "
|
||||
"Without --update-errors, photos that were successfully exported but generated "
|
||||
"an error or warning during export will not be re-attempted if metadata has not changed. "
|
||||
"Must be used with --update.",
|
||||
)
|
||||
@click.option(
|
||||
"--ignore-signature",
|
||||
is_flag=True,
|
||||
@@ -707,29 +706,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
help="Enable beta options.",
|
||||
)
|
||||
@click.option(
|
||||
"--profile",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
help="Run export with code profiler.",
|
||||
)
|
||||
@click.option(
|
||||
"--profile-sort",
|
||||
default=None,
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
multiple=True,
|
||||
metavar="SORT_KEY",
|
||||
type=click.Choice(
|
||||
PROFILE_SORT_KEYS,
|
||||
case_sensitive=True,
|
||||
),
|
||||
help="Sort profiler output by SORT_KEY as specified at https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats. "
|
||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@THEME_OPTION
|
||||
@DEBUG_OPTIONS
|
||||
@DB_ARGUMENT
|
||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||
@click.pass_obj
|
||||
@@ -752,33 +729,34 @@ def export(
|
||||
added_after,
|
||||
added_before,
|
||||
added_in_last,
|
||||
album_keyword,
|
||||
album,
|
||||
album_keyword,
|
||||
alt_copy,
|
||||
append,
|
||||
beta,
|
||||
burst,
|
||||
cleanup,
|
||||
cloudasset,
|
||||
config_only,
|
||||
convert_to_jpeg,
|
||||
current_name,
|
||||
deleted_only,
|
||||
deleted,
|
||||
description_template,
|
||||
deleted_only,
|
||||
description,
|
||||
description_template,
|
||||
dest,
|
||||
directory,
|
||||
download_missing,
|
||||
dry_run,
|
||||
duplicate,
|
||||
edited_suffix,
|
||||
edited,
|
||||
edited_suffix,
|
||||
exif,
|
||||
exiftool,
|
||||
exiftool_merge_keywords,
|
||||
exiftool_merge_persons,
|
||||
exiftool_option,
|
||||
exiftool_path,
|
||||
exiftool,
|
||||
export_as_hardlink,
|
||||
export_by_date,
|
||||
exportdb,
|
||||
@@ -801,12 +779,13 @@ def export(
|
||||
ignore_date_modified,
|
||||
ignore_signature,
|
||||
in_album,
|
||||
incloud,
|
||||
is_reference,
|
||||
jpeg_ext,
|
||||
jpeg_quality,
|
||||
keep,
|
||||
keyword_template,
|
||||
keyword,
|
||||
keyword_template,
|
||||
label,
|
||||
limit,
|
||||
live,
|
||||
@@ -818,18 +797,21 @@ def export(
|
||||
name,
|
||||
no_comment,
|
||||
no_description,
|
||||
no_keyword,
|
||||
no_likes,
|
||||
no_location,
|
||||
no_keyword,
|
||||
no_place,
|
||||
no_progress,
|
||||
no_title,
|
||||
not_burst,
|
||||
not_cloudasset,
|
||||
not_favorite,
|
||||
not_hdr,
|
||||
not_hidden,
|
||||
not_in_album,
|
||||
not_incloud,
|
||||
not_live,
|
||||
not_missing,
|
||||
not_panorama,
|
||||
not_portrait,
|
||||
not_reference,
|
||||
@@ -844,18 +826,16 @@ def export(
|
||||
original_suffix,
|
||||
overwrite,
|
||||
panorama,
|
||||
person_keyword,
|
||||
person,
|
||||
person_keyword,
|
||||
place,
|
||||
portrait,
|
||||
post_command,
|
||||
post_function,
|
||||
preview,
|
||||
preview_if_missing,
|
||||
preview_suffix,
|
||||
preview,
|
||||
print_template,
|
||||
profile_sort,
|
||||
profile,
|
||||
query_eval,
|
||||
query_function,
|
||||
ramdb,
|
||||
@@ -868,15 +848,15 @@ def export(
|
||||
selected,
|
||||
selfie,
|
||||
shared,
|
||||
sidecar_drop_ext,
|
||||
sidecar,
|
||||
sidecar_drop_ext,
|
||||
skip_bursts,
|
||||
skip_edited,
|
||||
skip_live,
|
||||
skip_original_if_edited,
|
||||
skip_raw,
|
||||
skip_uuid_from_file,
|
||||
skip_uuid,
|
||||
skip_uuid_from_file,
|
||||
slow_mo,
|
||||
strip,
|
||||
theme,
|
||||
@@ -888,24 +868,35 @@ def export(
|
||||
to_time,
|
||||
touch_file,
|
||||
update,
|
||||
update_errors,
|
||||
use_photokit,
|
||||
use_photos_export,
|
||||
uti,
|
||||
uuid_from_file,
|
||||
uuid,
|
||||
uuid_from_file,
|
||||
verbose,
|
||||
xattr_template,
|
||||
year,
|
||||
debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||
watch,
|
||||
breakpoint,
|
||||
# debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||
# watch,
|
||||
# breakpoint,
|
||||
):
|
||||
"""Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
|
||||
Optionally, query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
if more than one different option is provided, they are treated as "AND"
|
||||
(e.g. search for photos matching all options).
|
||||
If the same query option is provided multiple times, they are treated as
|
||||
"OR" (e.g. search for photos matching any of the options).
|
||||
If no query options are provided, all photos will be exported.
|
||||
|
||||
For example, adding the query options:
|
||||
|
||||
--person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will export all photos with either person of ("John Doe" OR "Jane Doe") AND keyword of "vacation"
|
||||
|
||||
By default, all versions of all photos will be exported including edited
|
||||
versions, live photo movies, burst photos, and associated raw images.
|
||||
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options
|
||||
@@ -914,27 +905,8 @@ def export(
|
||||
|
||||
# capture locals for use with ConfigOptions before changing any of them
|
||||
locals_ = locals()
|
||||
|
||||
set_crash_data("locals", locals_)
|
||||
|
||||
if profile:
|
||||
click.echo("Profiling...")
|
||||
profile_sort = profile_sort or ["cumulative"]
|
||||
click.echo(f"Profile sort_stats order: {profile_sort}")
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
def at_exit():
|
||||
pr.disable()
|
||||
click.echo("Profiling completed")
|
||||
s = io.StringIO()
|
||||
pstats.Stats(pr, stream=s).strip_dirs().sort_stats(
|
||||
*profile_sort
|
||||
).print_stats()
|
||||
click.echo(s.getvalue())
|
||||
|
||||
atexit.register(at_exit)
|
||||
|
||||
# NOTE: because of the way ConfigOptions works, Click options must not
|
||||
# set defaults which are not None or False. If defaults need to be set
|
||||
# do so below after load_config and save_config are handled.
|
||||
@@ -971,12 +943,13 @@ def export(
|
||||
|
||||
# re-set the local vars to the corresponding config value
|
||||
# this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter
|
||||
added_after = cfg.added_after
|
||||
added_before = cfg.added_before
|
||||
added_in_last = cfg.added_in_last
|
||||
|
||||
add_exported_to_album = cfg.add_exported_to_album
|
||||
add_missing_to_album = cfg.add_missing_to_album
|
||||
add_skipped_to_album = cfg.add_skipped_to_album
|
||||
added_after = cfg.added_after
|
||||
added_before = cfg.added_before
|
||||
added_in_last = cfg.added_in_last
|
||||
album = cfg.album
|
||||
album_keyword = cfg.album_keyword
|
||||
alt_copy = cfg.alt_copy
|
||||
@@ -984,6 +957,7 @@ def export(
|
||||
beta = cfg.beta
|
||||
burst = cfg.burst
|
||||
cleanup = cfg.cleanup
|
||||
cloudasset = cfg.cloudasset
|
||||
convert_to_jpeg = cfg.convert_to_jpeg
|
||||
current_name = cfg.current_name
|
||||
db = cfg.db
|
||||
@@ -1025,6 +999,7 @@ def export(
|
||||
ignore_date_modified = cfg.ignore_date_modified
|
||||
ignore_signature = cfg.ignore_signature
|
||||
in_album = cfg.in_album
|
||||
incloud = cfg.incloud
|
||||
is_reference = cfg.is_reference
|
||||
jpeg_ext = cfg.jpeg_ext
|
||||
jpeg_quality = cfg.jpeg_quality
|
||||
@@ -1041,18 +1016,21 @@ def export(
|
||||
name = cfg.name
|
||||
no_comment = cfg.no_comment
|
||||
no_description = cfg.no_description
|
||||
no_keyword = cfg.no_keyword
|
||||
no_likes = cfg.no_likes
|
||||
no_location = cfg.no_location
|
||||
no_keyword = cfg.no_keyword
|
||||
no_place = cfg.no_place
|
||||
no_progress = cfg.no_progress
|
||||
no_title = cfg.no_title
|
||||
not_burst = cfg.not_burst
|
||||
not_cloudasset = cfg.not_cloudasset
|
||||
not_favorite = cfg.not_favorite
|
||||
not_hdr = cfg.not_hdr
|
||||
not_hidden = cfg.not_hidden
|
||||
not_in_album = cfg.not_in_album
|
||||
not_incloud = cfg.not_incloud
|
||||
not_live = cfg.not_live
|
||||
not_missing = cfg.not_missing
|
||||
not_panorama = cfg.not_panorama
|
||||
not_portrait = cfg.not_portrait
|
||||
not_reference = cfg.not_reference
|
||||
@@ -1109,6 +1087,7 @@ def export(
|
||||
to_time = cfg.to_time
|
||||
touch_file = cfg.touch_file
|
||||
update = cfg.update
|
||||
update_errors = cfg.update_errors
|
||||
use_photokit = cfg.use_photokit
|
||||
use_photos_export = cfg.use_photos_export
|
||||
uti = cfg.uti
|
||||
@@ -1117,7 +1096,6 @@ def export(
|
||||
verbose = cfg.verbose
|
||||
xattr_template = cfg.xattr_template
|
||||
year = cfg.year
|
||||
|
||||
# config file might have changed verbose
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
@@ -1136,6 +1114,7 @@ def export(
|
||||
# validate options
|
||||
exclusive_options = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("deleted", "deleted_only"),
|
||||
("description", "no_description"),
|
||||
("export_as_hardlink", "convert_to_jpeg"),
|
||||
@@ -1148,9 +1127,12 @@ def export(
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("is_reference", "not_reference"),
|
||||
("keyword", "no_keyword"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("keyword", "no_keyword"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("place", "no_place"),
|
||||
@@ -1162,19 +1144,19 @@ def export(
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("title", "no_title"),
|
||||
("is_reference", "not_reference"),
|
||||
]
|
||||
dependent_options = [
|
||||
("append", ("report")),
|
||||
("exiftool_merge_keywords", ("exiftool", "sidecar")),
|
||||
("exiftool_merge_persons", ("exiftool", "sidecar")),
|
||||
("favorite_rating", ("exiftool", "sidecar")),
|
||||
("exiftool_option", ("exiftool")),
|
||||
("favorite_rating", ("exiftool", "sidecar")),
|
||||
("ignore_signature", ("update", "force_update")),
|
||||
("jpeg_quality", ("convert_to_jpeg")),
|
||||
("keep", ("cleanup")),
|
||||
("missing", ("download_missing", "use_photos_export")),
|
||||
("only_new", ("update", "force_update")),
|
||||
("append", ("report")),
|
||||
("update_errors", ("update")),
|
||||
]
|
||||
try:
|
||||
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
||||
@@ -1235,7 +1217,7 @@ def export(
|
||||
|
||||
if report:
|
||||
report = render_and_validate_report(report, exiftool_path, dest)
|
||||
report_writer = report_writer_factory(report, append)
|
||||
report_writer = export_report_writer_factory(report, append)
|
||||
else:
|
||||
report_writer = ReportWriterNoOp()
|
||||
|
||||
@@ -1374,7 +1356,7 @@ def export(
|
||||
album=album,
|
||||
burst_photos=export_bursts,
|
||||
burst=burst,
|
||||
cloudasset=False,
|
||||
cloudasset=cloudasset,
|
||||
deleted_only=deleted_only,
|
||||
deleted=deleted,
|
||||
description=description,
|
||||
@@ -1394,7 +1376,7 @@ def export(
|
||||
hidden=hidden,
|
||||
ignore_case=ignore_case,
|
||||
in_album=in_album,
|
||||
incloud=False,
|
||||
incloud=incloud,
|
||||
is_reference=is_reference,
|
||||
keyword=keyword,
|
||||
label=label,
|
||||
@@ -1415,14 +1397,14 @@ def export(
|
||||
no_place=no_place,
|
||||
no_title=no_title,
|
||||
not_burst=not_burst,
|
||||
not_cloudasset=False,
|
||||
not_cloudasset=not_cloudasset,
|
||||
not_favorite=not_favorite,
|
||||
not_hdr=not_hdr,
|
||||
not_hidden=not_hidden,
|
||||
not_in_album=not_in_album,
|
||||
not_incloud=False,
|
||||
not_incloud=not_incloud,
|
||||
not_live=not_live,
|
||||
not_missing=None,
|
||||
not_missing=not_missing,
|
||||
not_panorama=not_panorama,
|
||||
not_portrait=not_portrait,
|
||||
not_reference=not_reference,
|
||||
@@ -1563,6 +1545,7 @@ def export(
|
||||
strip=strip,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
update_errors=update_errors,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose_=verbose_,
|
||||
@@ -1710,9 +1693,6 @@ def export(
|
||||
progress.advance(task, num_photos - photo_num)
|
||||
break
|
||||
|
||||
# store results so they can be used by `osxphotos exportdb --report`
|
||||
export_db.set_export_results(results)
|
||||
|
||||
photo_str_total = pluralize(len(photos), "photo", "photos")
|
||||
if update or force_update:
|
||||
summary = (
|
||||
@@ -1787,6 +1767,9 @@ def export(
|
||||
results.deleted_files = cleaned_files
|
||||
results.deleted_directories = cleaned_dirs
|
||||
|
||||
# store results so they can be used by `osxphotos exportdb --report`
|
||||
export_db.set_export_results(results)
|
||||
|
||||
if report:
|
||||
verbose_(f"Wrote export report to [filepath]{report}")
|
||||
report_writer.close()
|
||||
@@ -1849,6 +1832,7 @@ def export_photo(
|
||||
photo_num=1,
|
||||
num_photos=1,
|
||||
tmpdir=None,
|
||||
update_errors=False,
|
||||
):
|
||||
"""Helper function for export that does the actual export
|
||||
|
||||
@@ -1895,6 +1879,7 @@ def export_photo(
|
||||
skip_original_if_edited: bool; if True does not export original if photo has been edited
|
||||
touch_file: bool; sets file's modification time to match photo date
|
||||
update: bool, only export updated photos
|
||||
update_errors: bool, attempt to re-export photos that previously produced errors even if they otherwise would not be exported
|
||||
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
|
||||
verbose_: callable for verbose output
|
||||
tmpdir: optional str; temporary directory to use for export
|
||||
@@ -2060,6 +2045,7 @@ def export_photo(
|
||||
sidecar_flags=sidecar_flags,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
update_errors=update_errors,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose_=verbose_,
|
||||
@@ -2175,6 +2161,7 @@ def export_photo(
|
||||
sidecar_flags=sidecar_flags if not export_original else 0,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
update_errors=update_errors,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose_=verbose_,
|
||||
@@ -2261,6 +2248,7 @@ def export_photo_to_directory(
|
||||
sidecar_flags,
|
||||
touch_file,
|
||||
update,
|
||||
update_errors,
|
||||
use_photos_export,
|
||||
use_photokit,
|
||||
verbose_,
|
||||
@@ -2298,8 +2286,8 @@ def export_photo_to_directory(
|
||||
download_missing=download_missing,
|
||||
dry_run=dry_run,
|
||||
edited=edited,
|
||||
exiftool_flags=exiftool_option,
|
||||
exiftool=exiftool,
|
||||
exiftool_flags=exiftool_option,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
export_db=export_db,
|
||||
favorite_rating=favorite_rating,
|
||||
@@ -2314,22 +2302,23 @@ def export_photo_to_directory(
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
overwrite=overwrite,
|
||||
preview_suffix=preview_suffix,
|
||||
preview=export_preview or (missing and preview_if_missing),
|
||||
preview_suffix=preview_suffix,
|
||||
raw_photo=export_raw,
|
||||
render_options=render_options,
|
||||
replace_keywords=replace_keywords,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
rich=True,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
tmpdir=tmpdir,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
update_errors=update_errors,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose=verbose_,
|
||||
tmpdir=tmpdir,
|
||||
rich=True,
|
||||
)
|
||||
exporter = PhotoExporter(photo)
|
||||
export_results = exporter.export(
|
||||
|
||||
@@ -16,6 +16,7 @@ from osxphotos.export_db import (
|
||||
)
|
||||
from osxphotos.export_db_utils import (
|
||||
export_db_check_signatures,
|
||||
export_db_get_errors,
|
||||
export_db_get_last_run,
|
||||
export_db_get_version,
|
||||
export_db_save_config_to_file,
|
||||
@@ -25,10 +26,18 @@ from osxphotos.export_db_utils import (
|
||||
)
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .export import render_and_validate_report
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import report_writer_factory
|
||||
from .verbose import verbose_print
|
||||
from .report_writer import export_report_writer_factory
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
|
||||
@click.command(name="exportdb")
|
||||
@@ -65,6 +74,16 @@ from .verbose import verbose_print
|
||||
nargs=1,
|
||||
help="Print information about FILE_PATH contained in the database.",
|
||||
)
|
||||
@click.option(
|
||||
"--errors",
|
||||
is_flag=True,
|
||||
help="Print list of files that had warnings/errors on export (from all runs).",
|
||||
)
|
||||
@click.option(
|
||||
"--last-errors",
|
||||
is_flag=True,
|
||||
help="Print list of files that had warnings/errors on last export run.",
|
||||
)
|
||||
@click.option(
|
||||
"--uuid-files",
|
||||
metavar="UUID",
|
||||
@@ -145,6 +164,8 @@ def exportdb(
|
||||
export_db,
|
||||
export_dir,
|
||||
info,
|
||||
errors,
|
||||
last_errors,
|
||||
last_run,
|
||||
migrate,
|
||||
report,
|
||||
@@ -161,13 +182,18 @@ def exportdb(
|
||||
version,
|
||||
):
|
||||
"""Utilities for working with the osxphotos export database"""
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
color_theme = get_theme()
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp=False, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console(theme=color_theme))
|
||||
set_rich_theme(color_theme)
|
||||
|
||||
# validate options and args
|
||||
if append and not report:
|
||||
print(
|
||||
"[red]Error: --append requires --report; ee --help for more information.[/]",
|
||||
rich_echo(
|
||||
"[error]Error: --append requires --report; ee --help for more information.[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -177,8 +203,8 @@ def exportdb(
|
||||
# assume it's the export folder
|
||||
export_db = export_db / OSXPHOTOS_EXPORT_DB
|
||||
if not export_db.is_file():
|
||||
print(
|
||||
f"[red]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/red]"
|
||||
rich_echo(
|
||||
f"[error]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -203,7 +229,7 @@ def exportdb(
|
||||
]
|
||||
]
|
||||
if sum(sub_commands) > 1:
|
||||
print("[red]Only a single sub-command may be specified at a time[/red]")
|
||||
rich_echo("[error]Only a single sub-command may be specified at a time[/error]")
|
||||
sys.exit(1)
|
||||
|
||||
# process sub-commands
|
||||
@@ -212,11 +238,13 @@ def exportdb(
|
||||
try:
|
||||
osxphotos_ver, export_db_ver = export_db_get_version(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: could not read version from {export_db}: {e}[/red]")
|
||||
rich_echo(
|
||||
f"[error]Error: could not read version from {export_db}: {e}[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"osxphotos version: {osxphotos_ver}, export database version: {export_db_ver}"
|
||||
rich_echo(
|
||||
f"osxphotos version: [num]{osxphotos_ver}[/], export database version: [num]{export_db_ver}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -225,11 +253,11 @@ def exportdb(
|
||||
start_size = pathlib.Path(export_db).stat().st_size
|
||||
export_db_vacuum(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Vacuumed {export_db}! {start_size} bytes -> {pathlib.Path(export_db).stat().st_size} bytes"
|
||||
rich_echo(
|
||||
f"Vacuumed {export_db}! [num]{start_size}[/] bytes -> [num]{pathlib.Path(export_db).stat().st_size}[/] bytes"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -239,31 +267,33 @@ def exportdb(
|
||||
export_db, export_dir, verbose_, dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Done. Updated {updated} files, skipped {skipped} files.")
|
||||
rich_echo(
|
||||
f"Done. Updated [num]{updated}[/] files, skipped [num]{skipped}[/] files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if last_run:
|
||||
try:
|
||||
last_run_info = export_db_get_last_run(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"last run at {last_run_info[0]}:")
|
||||
print(f"osxphotos {last_run_info[1]}")
|
||||
rich_echo(f"last run at [time]{last_run_info[0]}:")
|
||||
rich_echo(f"osxphotos {last_run_info[1]}")
|
||||
sys.exit(0)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
export_db_save_config_to_file(export_db, save_config)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Saved configuration to {save_config}")
|
||||
rich_echo(f"Saved configuration to [filepath]{save_config}")
|
||||
sys.exit(0)
|
||||
|
||||
if check_signatures:
|
||||
@@ -272,11 +302,12 @@ def exportdb(
|
||||
export_db, export_dir, verbose_=verbose_
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Done. Found {matched} matching signatures and {notmatched} signatures that don't match. Skipped {skipped} missing files."
|
||||
rich_echo(
|
||||
f"Done. Found [num]{matched}[/] matching signatures and [num]{notmatched}[/] signatures that don't match. "
|
||||
f"Skipped [num]{skipped}[/] missing files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -286,11 +317,12 @@ def exportdb(
|
||||
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Done. Touched {touched} files, skipped {not_touched} up to date files, skipped {skipped} missing files."
|
||||
rich_echo(
|
||||
f"Done. Touched [num]{touched}[/] files, skipped [num]{not_touched}[/] up to date files, "
|
||||
f"skipped [num]{skipped}[/] missing files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -299,28 +331,63 @@ def exportdb(
|
||||
try:
|
||||
info_rec = exportdb.get_file_record(info)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if info_rec:
|
||||
# use rich print as rich_echo doesn't highlight json
|
||||
print(info_rec.json(indent=2))
|
||||
else:
|
||||
print(f"[red]File '{info}' not found in export database[/red]")
|
||||
rich_echo(f"[error]File '{info}' not found in export database[/error]")
|
||||
sys.exit(0)
|
||||
|
||||
if errors:
|
||||
# list errors
|
||||
try:
|
||||
error_list = export_db_get_errors(export_db)
|
||||
except Exception as e:
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if error_list:
|
||||
for error in error_list:
|
||||
rich_echo(error)
|
||||
else:
|
||||
rich_echo("No errors found")
|
||||
sys.exit(0)
|
||||
|
||||
if last_errors:
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
if export_results := exportdb.get_export_results(0):
|
||||
for error in [
|
||||
*export_results.error,
|
||||
*export_results.exiftool_error,
|
||||
*export_results.exiftool_warning,
|
||||
]:
|
||||
rich_click_echo(
|
||||
f"[filepath]{error[0]}[/], [time]{export_results.datetime}[/], [error]{error[1]}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
else:
|
||||
rich_echo_error("[error]Results from last run not found in database[/]")
|
||||
sys.exit(1)
|
||||
|
||||
if uuid_info:
|
||||
# get photoinfo record for a uuid
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
try:
|
||||
info_rec = exportdb.get_photoinfo_for_uuid(uuid_info)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if info_rec:
|
||||
# use rich print as rich_echo doesn't highlight json
|
||||
print(json.dumps(json.loads(info_rec), sort_keys=True, indent=2))
|
||||
else:
|
||||
print(f"[red]UUID '{uuid_info}' not found in export database[/red]")
|
||||
rich_echo(
|
||||
f"[error]UUID '{uuid_info}' not found in export database[/error]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if uuid_files:
|
||||
@@ -329,32 +396,38 @@ def exportdb(
|
||||
try:
|
||||
file_list = exportdb.get_files_for_uuid(uuid_files)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if file_list:
|
||||
for f in file_list:
|
||||
print(f)
|
||||
rich_echo(f"[filepath]{f}[/]")
|
||||
else:
|
||||
print(f"[red]UUID '{uuid_files}' not found in export database[/red]")
|
||||
rich_echo(
|
||||
f"[error]UUID '{uuid_files}' not found in export database[/error]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if delete_uuid:
|
||||
# delete a uuid from the export database
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
for uuid in delete_uuid:
|
||||
print(f"Deleting uuid {uuid} from database.")
|
||||
rich_echo(f"Deleting uuid [uuid]{uuid}[/] from database.")
|
||||
count = exportdb.delete_data_for_uuid(uuid)
|
||||
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
|
||||
rich_echo(
|
||||
f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if delete_file:
|
||||
# delete information associated with a file from the export database
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
for filepath in delete_file:
|
||||
print(f"Deleting file {filepath} from database.")
|
||||
rich_echo(f"Deleting file [filepath]{filepath}[/] from database.")
|
||||
count = exportdb.delete_data_for_filepath(filepath)
|
||||
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
|
||||
rich_echo(
|
||||
f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if report:
|
||||
@@ -363,27 +436,27 @@ def exportdb(
|
||||
report_filename = render_and_validate_report(report_template, "", export_dir)
|
||||
export_results = exportdb.get_export_results(run_id)
|
||||
if not export_results:
|
||||
print(f"[red]No report results found for run ID {run_id}[/red]")
|
||||
rich_echo(f"[error]No report results found for run ID {run_id}[/error]")
|
||||
sys.exit(1)
|
||||
try:
|
||||
report_writer = report_writer_factory(report_filename, append=append)
|
||||
report_writer = export_report_writer_factory(report_filename, append=append)
|
||||
except ValueError as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
report_writer.write(export_results)
|
||||
report_writer.close()
|
||||
print(f"Wrote report to {report_filename}")
|
||||
rich_echo(f"Wrote report to [filepath]{report_filename}[/]")
|
||||
sys.exit(0)
|
||||
|
||||
if migrate:
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
if upgraded := exportdb.was_upgraded:
|
||||
print(
|
||||
f"Migrated export database {export_db} from version {upgraded[0]} to {upgraded[1]}"
|
||||
rich_echo(
|
||||
f"Migrated export database [filepath]{export_db}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Export database {export_db} is already at latest version {OSXPHOTOS_EXPORTDB_VERSION}"
|
||||
rich_echo(
|
||||
f"Export database [filepath]{export_db}[/] is already at latest version [num]{OSXPHOTOS_EXPORTDB_VERSION}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -393,7 +466,7 @@ def exportdb(
|
||||
c = exportdb._conn.cursor()
|
||||
results = c.execute(sql)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
for row in results:
|
||||
|
||||
@@ -26,13 +26,20 @@ import click
|
||||
from photoscript import Photo, PhotosLibrary
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from strpdatetime import strpdatetime
|
||||
|
||||
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.cli.common import get_data_dir
|
||||
from osxphotos.cli.help import HELP_WIDTH
|
||||
from osxphotos.cli.param_types import TemplateString
|
||||
from osxphotos.datetime_utils import datetime_naive_to_local
|
||||
from osxphotos.cli.param_types import FunctionCall, StrpDateTimePattern, TemplateString
|
||||
from osxphotos.datetime_utils import (
|
||||
datetime_has_tz,
|
||||
datetime_naive_to_local,
|
||||
datetime_remove_tz,
|
||||
datetime_tz_to_utc,
|
||||
datetime_utc_to_local,
|
||||
)
|
||||
from osxphotos.exiftool import ExifToolCaching, get_exiftool_path
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
@@ -42,6 +49,7 @@ from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
@@ -89,7 +97,7 @@ class PhotoInfoFromFile:
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""Use file creation date and local timezone"""
|
||||
"""Use file creation date and local time zone"""
|
||||
ctime = os.path.getctime(self._path)
|
||||
dt = datetime.datetime.fromtimestamp(ctime)
|
||||
return datetime_naive_to_local(dt)
|
||||
@@ -311,6 +319,8 @@ def location_from_file(
|
||||
latitude = -latitude
|
||||
elif latitude_ref != "N":
|
||||
latitude = None
|
||||
if latitude is None:
|
||||
latitude = metadata.get("XMP:GPSLatitude")
|
||||
if longitude := metadata.get("EXIF:GPSLongitude"):
|
||||
longitude = float(longitude)
|
||||
longitude_ref = metadata.get("EXIF:GPSLongitudeRef")
|
||||
@@ -318,6 +328,8 @@ def location_from_file(
|
||||
longitude = -longitude
|
||||
elif longitude_ref != "E":
|
||||
longitude = None
|
||||
if longitude is None:
|
||||
longitude = metadata.get("XMP:GPSLongitude")
|
||||
if latitude is None or longitude is None:
|
||||
# maybe it's a video
|
||||
if lat_lon := metadata.get("QuickTime:GPSCoordinates") or metadata.get(
|
||||
@@ -463,6 +475,33 @@ def set_photo_location(
|
||||
photo.location = location
|
||||
|
||||
|
||||
def set_photo_date_from_filename(
|
||||
photo: Photo, filepath: Path, parse_date: str, verbose: Callable[..., None]
|
||||
):
|
||||
"""Set date of photo from filename"""
|
||||
# TODO: handle timezone (use code from timewarp), for now convert timezone to local timezone
|
||||
try:
|
||||
date = strpdatetime(filepath.name, parse_date)
|
||||
# Photo.date must be timezone naive (assumed to local timezone)
|
||||
if datetime_has_tz(date):
|
||||
local_date = datetime_remove_tz(
|
||||
datetime_utc_to_local(datetime_tz_to_utc(date))
|
||||
)
|
||||
verbose(
|
||||
f"Moving date with timezone [time]{date}[/] to local timezone: [time]{local_date.strftime('%Y-%m-%d %H:%M:%S')}[/]"
|
||||
)
|
||||
date = local_date
|
||||
except ValueError:
|
||||
verbose(
|
||||
f"[warning]Could not parse date from filename [filename]{filepath.name}[/][/]"
|
||||
)
|
||||
return
|
||||
verbose(
|
||||
f"Setting date of photo [filename]{filepath.name}[/] to [time]{date.strftime('%Y-%m-%d %H:%M:%S')}[/]"
|
||||
)
|
||||
photo.date = date
|
||||
|
||||
|
||||
def get_relative_filepath(filepath: Path, relative_to: Optional[str]) -> Path:
|
||||
"""Get relative filepath of file relative to relative_to or return filepath if relative_to is None
|
||||
|
||||
@@ -499,6 +538,7 @@ def check_templates_and_exit(
|
||||
album: Tuple[str],
|
||||
exiftool_path: Optional[str],
|
||||
exiftool: bool,
|
||||
parse_date: Optional[str],
|
||||
):
|
||||
"""Renders templates against each file so user can verify correctness"""
|
||||
for file in files:
|
||||
@@ -539,6 +579,14 @@ def check_templates_and_exit(
|
||||
)
|
||||
rendered_album = rendered_album[0] if rendered_album else "None"
|
||||
echo(f"album: [italic]{al}[/]: {rendered_album}")
|
||||
if parse_date:
|
||||
try:
|
||||
date = strpdatetime(file.name, parse_date)
|
||||
echo(f"date: [italic]{parse_date}[/]: {date}")
|
||||
except ValueError:
|
||||
echo(
|
||||
f"[warning]Could not parse date from filename [filename]{file.name}[/][/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -1042,6 +1090,70 @@ class ImportCommand(click.Command):
|
||||
but will instead print out the rendered value for each `--title`, `--description`,
|
||||
`--keyword`, and `--album` option. It will also print out the values extracted by
|
||||
the `--exiftool` option.
|
||||
|
||||
## Parsing Dates/Times from Filenames
|
||||
|
||||
The --parse-date option allows you to parse dates/times from the filename of the
|
||||
file being imported. This is useful if you have a large number of files with
|
||||
dates/times embedded in the filename but not in the metadata.
|
||||
|
||||
The argument to `--parse-date` is a pattern string that is used to parse the date/time
|
||||
from the filename. The pattern string is a superset of the python `strftime/strptime`
|
||||
format with the following additions:
|
||||
|
||||
- *: Match any number of characters
|
||||
- ^: Match the beginning of the string
|
||||
- $: Match the end of the string
|
||||
- {n}: Match exactly n characters
|
||||
- {n,}: Match at least n characters
|
||||
- {n,m}: Match at least n characters and at most m characters
|
||||
- In addition to `%%` for a literal `%`, the following format codes are supported:
|
||||
`%^`, `%$`, `%*`, `%|`, `%{`, `%}` for `^`, `$`, `*`, `|`, `{`, `}` respectively
|
||||
- |: join multiple format codes; each code is tried in order until one matches
|
||||
- Unlike the standard library, the leading zero is not optional for
|
||||
%d, %m, %H, %I, %M, %S, %j, %U, %W, and %V
|
||||
- For optional leading zero, use %-d, %-m, %-H, %-I, %-M, %-S, %-j, %-U, %-W, and %-V
|
||||
|
||||
For more information on strptime format codes, see:
|
||||
https://docs.python.org/3/library/datetime.html?highlight=strptime#strftime-and-strptime-format-codes
|
||||
|
||||
**Note**: The time zone of the parsed date/time is assumed to be the local time zone.
|
||||
If the parse pattern includes a time zone, the photo's time will be converted from
|
||||
the specified time zone to the local time zone. osxphotos import does not
|
||||
currently support setting the time zone of imported photos.
|
||||
See also `osxphotos help timewarp` for more information on the timewarp
|
||||
command which can be used to change the time zone of photos after import.
|
||||
|
||||
### Examples
|
||||
|
||||
If you have photos with embedded names in filenames like `IMG_1234_20200322_123456.jpg`
|
||||
and `12345678_20200322.jpg`, you can parse the dates with the following pattern:
|
||||
`--parse-date "IMG_*_%Y%m%d_%H%M%S|*_%Y%m%d.*"`. The first pattern matches the first format
|
||||
and the second pattern matches the second. The `|` character is used to separate the two
|
||||
patterns. The order is important as the first pattern will be tried first then the second
|
||||
and so on. If you have multiple formats in your filenames you will want to order the patterns
|
||||
from most specific to least specific to avoid false matches.
|
||||
|
||||
## Post Function
|
||||
|
||||
You can run a custom python function after each photo is imported using `--post-function`.
|
||||
The format is `osxphotos import /file/to/import --post-function post_function.py::post_function`
|
||||
where `post_function.py` is the name of the python file containing the function and `post_function`
|
||||
is the name of the function. The function will be called with the following arguments:
|
||||
`post_function(photo: photoscript.Photo, filepath: pathlib.Path, verbose: t.Callable, **kwargs)`
|
||||
|
||||
- photo: photoscript.Photo instance for the photo that's just been imported
|
||||
- filepath: pathlib.Path to the file that was imported (this is the path to the source file, not the path inside the Photos library)
|
||||
- verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
- **kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
The function will get called immediately after the photo has been imported into Photos
|
||||
and all metadata been set (e.g. --exiftool, --title, etc.)
|
||||
|
||||
You may call more than one function by repeating the `--post-function` option.
|
||||
|
||||
See https://rhettbull.github.io/PhotoScript/
|
||||
for documentation on photoscript and the Photo class that is passed to the function.
|
||||
"""
|
||||
)
|
||||
console = Console()
|
||||
@@ -1113,6 +1225,21 @@ class ImportCommand(click.Command):
|
||||
"Longitude is a number in the range -180.0 to 180.0; "
|
||||
"positive longitudes are east of the Prime Meridian; negative longitudes are west of the Prime Meridian.",
|
||||
)
|
||||
@click.option(
|
||||
"--parse-date",
|
||||
"-P",
|
||||
metavar="DATE_PATTERN",
|
||||
type=StrpDateTimePattern(),
|
||||
help="Parse date from filename using DATE_PATTERN. "
|
||||
"If file does not match DATE_PATTERN, the date will be set by Photos using Photo's default behavior. "
|
||||
"DATE_PATTERN is a strptime-compatible pattern with extensions as pattern described below. "
|
||||
"If DATE_PATTERN matches time zone information, the time will be set to the local time in the timezone "
|
||||
"as the import command does not yet support setting time zone information. "
|
||||
"For example, if your photos are named 'IMG_1234_2022_11_23_12_34_56.jpg' where the date/time is "
|
||||
"'2022-11-23 12:34:56', you could use the pattern '%Y_%m_%d_%H_%M_%S' or "
|
||||
"'IMG_*_%Y_%m_%d_%H_%M_%S' to further narrow the pattern to only match files with 'IMG_xxxx_' in the name."
|
||||
"See also --check-templates.",
|
||||
)
|
||||
@click.option(
|
||||
"--clear-metadata",
|
||||
"-C",
|
||||
@@ -1217,7 +1344,20 @@ class ImportCommand(click.Command):
|
||||
"--check-templates",
|
||||
is_flag=True,
|
||||
help="Don't actually import anything; "
|
||||
"renders template strings so you can verify they are correct.",
|
||||
"renders template strings and date patterns so you can verify they are correct.",
|
||||
)
|
||||
@click.option(
|
||||
"--post-function",
|
||||
metavar="filename.py::function",
|
||||
nargs=1,
|
||||
type=FunctionCall(),
|
||||
multiple=True,
|
||||
help="Run python function after importing file."
|
||||
"Use this in format: --post-function filename.py::function where filename.py is a python "
|
||||
"file you've created and function is the name of the function in the python file you want to call. "
|
||||
"The function will be passed a reference to the photo object and the path to the file that was imported. "
|
||||
"You can run more than one function by repeating the '--post-function' option with different arguments. "
|
||||
"See Post Function below.",
|
||||
)
|
||||
@THEME_OPTION
|
||||
@click.argument("files", nargs=-1)
|
||||
@@ -1241,6 +1381,8 @@ def import_cli(
|
||||
location,
|
||||
merge_keywords,
|
||||
no_progress,
|
||||
parse_date,
|
||||
post_function,
|
||||
relative_to,
|
||||
report,
|
||||
resume,
|
||||
@@ -1289,6 +1431,7 @@ def import_cli(
|
||||
album,
|
||||
exiftool_path,
|
||||
exiftool,
|
||||
parse_date,
|
||||
)
|
||||
|
||||
# initialize report data
|
||||
@@ -1381,6 +1524,9 @@ def import_cli(
|
||||
if location:
|
||||
set_photo_location(photo, filepath, location, verbose)
|
||||
|
||||
if parse_date:
|
||||
set_photo_date_from_filename(photo, filepath, parse_date, verbose)
|
||||
|
||||
if album:
|
||||
add_photo_to_albums(
|
||||
photo,
|
||||
@@ -1392,6 +1538,17 @@ def import_cli(
|
||||
verbose,
|
||||
)
|
||||
|
||||
if post_function:
|
||||
for function in post_function:
|
||||
# post function is tuple of (function, filename.py::function_name)
|
||||
verbose(f"Calling post-function [bold]{function[1]}")
|
||||
try:
|
||||
function[0](photo, filepath, verbose)
|
||||
except Exception as e:
|
||||
rich_echo_error(
|
||||
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
|
||||
)
|
||||
|
||||
update_report_record(report_data[filepath], photo, filepath)
|
||||
import_db.set(str(filepath), report_data[filepath])
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
import bitmath
|
||||
import click
|
||||
import pytimeparse2
|
||||
from strpdatetime import strpdatetime
|
||||
|
||||
from osxphotos.export_db_utils import export_db_get_version
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
@@ -21,6 +22,7 @@ __all__ = [
|
||||
"DateTimeISO8601",
|
||||
"ExportDBType",
|
||||
"FunctionCall",
|
||||
"StrpDateTimePattern",
|
||||
"TemplateString",
|
||||
"TimeISO8601",
|
||||
"TimeOffset",
|
||||
@@ -217,3 +219,24 @@ class UTCOffset(click.ParamType):
|
||||
f"Invalid timezone format: {value}. "
|
||||
"Valid format for timezone offset: '±HH:MM', '±H:MM', or '±HHMM'"
|
||||
)
|
||||
|
||||
|
||||
class StrpDateTimePattern(click.ParamType):
|
||||
"""A pattern to be used with strpdatetime()"""
|
||||
|
||||
name = "STRPDATETIME_PATTERN"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
strpdatetime("", value)
|
||||
return value
|
||||
except ValueError as e:
|
||||
# ValueError could be due to no match or invalid pattern
|
||||
# only want to fail if invalid pattern
|
||||
if any(
|
||||
s in str(e)
|
||||
for s in ["Invalid format string", "bad directive", "stray %"]
|
||||
):
|
||||
self.fail(f"Invalid strpdatetime format string: {value}. {e}")
|
||||
else:
|
||||
return value
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""Inspect photos selected in Photos """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
import re
|
||||
from fractions import Fraction
|
||||
from multiprocessing import Process, Queue
|
||||
from queue import Empty
|
||||
from time import gmtime, sleep, strftime
|
||||
from typing import List, Optional, Tuple
|
||||
import pathlib
|
||||
from typing import Generator, List, Optional, Tuple
|
||||
|
||||
import bitmath
|
||||
import click
|
||||
@@ -35,6 +37,17 @@ bold = add_rich_markup_tag("bold")
|
||||
dim = add_rich_markup_tag("dim")
|
||||
|
||||
|
||||
def add_cyclic_color_tag(values: list[str]) -> Generator[str, None, None]:
|
||||
"""Add a rich markup tag to each str in values, cycling through a set of colors"""
|
||||
# reuse some colors already in the theme
|
||||
# these are chosen for contrast to easily associate scores and values
|
||||
colors = ["change", "count", "filepath", "filename"]
|
||||
color_tags = [add_rich_markup_tag(color) for color in colors]
|
||||
modidx = len(color_tags)
|
||||
for idx, val in enumerate(values):
|
||||
yield color_tags[idx % modidx](val)
|
||||
|
||||
|
||||
def extract_uuid(text: str) -> str:
|
||||
"""Extract a UUID from a string"""
|
||||
if match := re.search(
|
||||
@@ -191,9 +204,15 @@ def format_templates(photo: PhotoInfo, templates: List[str]) -> str:
|
||||
|
||||
def format_score_info(photo: PhotoInfo) -> str:
|
||||
"""Format score_info"""
|
||||
score_str = bold("Score: ")
|
||||
score_str = bold("Scores: ")
|
||||
if photo.score:
|
||||
score_str += f"[num]{photo.score.overall}[/]" if photo.score else "-"
|
||||
# add color tags to each key: value pair to easily associate keys/values
|
||||
score_values = add_cyclic_color_tag(
|
||||
[f"{k}: {float(v):.2f}" for k, v in photo.score.asdict().items()]
|
||||
)
|
||||
score_str += ", ".join(score_values)
|
||||
else:
|
||||
score_str += "-"
|
||||
return score_str
|
||||
|
||||
|
||||
@@ -245,14 +264,18 @@ def format_paths(photo: PhotoInfo) -> str:
|
||||
"""format photo paths for inspect_photo"""
|
||||
path_str = bold("Path original: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path)}[/]" if photo.path else "-"
|
||||
if photo.path_edited:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
|
||||
if photo.path_live_photo:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path live video: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_live_photo)}[/]"
|
||||
if photo.path_edited:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
|
||||
if photo.path_edited_live_photo:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited live video: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited_live_photo)}[/]"
|
||||
if photo.path_raw:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path raw: ")
|
||||
|
||||
@@ -37,32 +37,7 @@ from .verbose import get_verbose_console
|
||||
@JSON_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
|
||||
@click.option(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
)
|
||||
@click.option(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
)
|
||||
@click.option(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
)
|
||||
|
||||
@click.option(
|
||||
"--add-to-album",
|
||||
metavar="ALBUM",
|
||||
@@ -188,8 +163,16 @@ def query(
|
||||
debug, # handled in cli/__init__.py
|
||||
):
|
||||
"""Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
if more than one different option is provided, they are treated as "AND"
|
||||
(e.g. search for photos matching all options).
|
||||
If the same query option is provided multiple times, they are treated as
|
||||
"OR" (e.g. search for photos matching any of the options).
|
||||
|
||||
For example:
|
||||
|
||||
osxphotos query --person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will return all photos with either person of ("John Doe" OR "Jane Doe") AND keyword of "vacation"
|
||||
"""
|
||||
|
||||
# if no query terms, show help and return
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""repl command for osxphotos CLI"""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
@@ -23,16 +22,13 @@ from .common import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
IncompatibleQueryOptions,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
|
||||
|
||||
class IncompatibleQueryOptions(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@click.command(name="repl")
|
||||
@DB_OPTION
|
||||
@click.pass_obj
|
||||
@@ -53,32 +49,6 @@ class IncompatibleQueryOptions(Exception):
|
||||
)
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
|
||||
@click.option(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
)
|
||||
@click.option(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
)
|
||||
@click.option(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
)
|
||||
def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
|
||||
import logging
|
||||
@@ -111,7 +81,7 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
print("Getting photos")
|
||||
tic = time.perf_counter()
|
||||
try:
|
||||
query_options = _query_options_from_kwargs(**kwargs)
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions:
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
|
||||
@@ -237,99 +207,6 @@ def _spotlight_photo(photo: PhotoInfo):
|
||||
photo_.spotlight()
|
||||
|
||||
|
||||
def _query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
"""Validate query options and create a QueryOptions instance"""
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
"added_after",
|
||||
"added_before",
|
||||
"added_in_last",
|
||||
"album",
|
||||
"duplicate",
|
||||
"edited",
|
||||
"exif",
|
||||
"external_edit",
|
||||
"folder",
|
||||
"from_date",
|
||||
"from_time",
|
||||
"has_raw",
|
||||
"keyword",
|
||||
"label",
|
||||
"max_size",
|
||||
"min_size",
|
||||
"name",
|
||||
"person",
|
||||
"query_eval",
|
||||
"query_function",
|
||||
"regex",
|
||||
"selected",
|
||||
"to_date",
|
||||
"to_time",
|
||||
"uti",
|
||||
"uuid_from_file",
|
||||
"uuid",
|
||||
"year",
|
||||
]
|
||||
exclusive = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("deleted", "deleted_only"),
|
||||
("favorite", "not_favorite"),
|
||||
("has_comment", "no_comment"),
|
||||
("has_likes", "no_likes"),
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("keyword", "no_keyword"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("portrait", "not_portrait"),
|
||||
("screenshot", "not_screenshot"),
|
||||
("selfie", "not_selfie"),
|
||||
("shared", "not_shared"),
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("is_reference", "not_reference"),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
|
||||
[
|
||||
all([any(kwargs["title"]), kwargs["no_title"]]),
|
||||
all([any(kwargs["description"]), kwargs["no_description"]]),
|
||||
all([any(kwargs["place"]), kwargs["no_place"]]),
|
||||
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
|
||||
]
|
||||
):
|
||||
raise IncompatibleQueryOptions
|
||||
|
||||
# actually have something to query
|
||||
include_photos = True
|
||||
include_movies = True # default searches for everything
|
||||
if kwargs["only_movies"]:
|
||||
include_photos = False
|
||||
if kwargs["only_photos"]:
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
if kwargs["uuid_from_file"]:
|
||||
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
|
||||
query_dict = {field: kwargs.get(field) for field in query_fields}
|
||||
query_dict["photos"] = include_photos
|
||||
query_dict["movies"] = include_movies
|
||||
query_dict["uuid"] = uuid
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
|
||||
def _query_photos(photosdb: PhotosDB, query_options: QueryOptions) -> List:
|
||||
"""Query photos given a QueryOptions instance"""
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Report writer for the --report option of `osxphotos export`"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
@@ -15,20 +16,28 @@ from osxphotos.export_db import OSXPHOTOS_ABOUT_STRING
|
||||
from osxphotos.photoexporter import ExportResults
|
||||
from osxphotos.sqlite_utils import sqlite_columns
|
||||
|
||||
from .sync_results import SyncResults
|
||||
|
||||
__all__ = [
|
||||
"report_writer_factory",
|
||||
"ExportReportWriterCSV",
|
||||
"ExportReportWriterJSON",
|
||||
"ExportReportWriterSqlite",
|
||||
"ReportWriterABC",
|
||||
"ReportWriterCSV",
|
||||
"ReportWriterSqlite",
|
||||
"ReportWriterNoOp",
|
||||
"SyncReportWriterCSV",
|
||||
"SyncReportWriterJSON",
|
||||
"SyncReportWriterSqlite",
|
||||
"export_report_writer_factory",
|
||||
"sync_report_writer_factory",
|
||||
]
|
||||
|
||||
|
||||
# Abstract base class for report writers
|
||||
class ReportWriterABC(ABC):
|
||||
"""Abstract base class for report writers"""
|
||||
|
||||
@abstractmethod
|
||||
def write(self, export_results: ExportResults):
|
||||
def write(self, results: ExportResults | SyncResults):
|
||||
"""Write results to the output file"""
|
||||
pass
|
||||
|
||||
@@ -38,13 +47,16 @@ class ReportWriterABC(ABC):
|
||||
pass
|
||||
|
||||
|
||||
# Report writer that does nothing, used for --dry-run or when --report not specified
|
||||
|
||||
|
||||
class ReportWriterNoOp(ABC):
|
||||
"""Report writer that does nothing"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
def write(self, results: ExportResults | SyncResults):
|
||||
"""Write results to the output file"""
|
||||
pass
|
||||
|
||||
@@ -53,8 +65,9 @@ class ReportWriterNoOp(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class ReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file"""
|
||||
# Classes for writing ExportResults to report file
|
||||
class ExportReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file for export results"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -95,7 +108,7 @@ class ReportWriterCSV(ReportWriterABC):
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
all_results = prepare_results_for_writing(export_results)
|
||||
all_results = prepare_export_results_for_writing(export_results)
|
||||
for data in list(all_results.values()):
|
||||
self._csv_writer.writerow(data)
|
||||
self._output_fh.flush()
|
||||
@@ -109,8 +122,8 @@ class ReportWriterCSV(ReportWriterABC):
|
||||
self._output_fh.close()
|
||||
|
||||
|
||||
class ReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON report file"""
|
||||
class ExportReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON report file for export results"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -134,7 +147,9 @@ class ReportWriterJSON(ReportWriterABC):
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
all_results = prepare_results_for_writing(export_results, bool_values=True)
|
||||
all_results = prepare_export_results_for_writing(
|
||||
export_results, bool_values=True
|
||||
)
|
||||
for data in list(all_results.values()):
|
||||
if self._first_record_written:
|
||||
self._output_fh.write(",\n")
|
||||
@@ -153,8 +168,8 @@ class ReportWriterJSON(ReportWriterABC):
|
||||
self.close()
|
||||
|
||||
|
||||
class ReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite report file"""
|
||||
class ExportReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite report file for export data"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -173,7 +188,7 @@ class ReportWriterSQLite(ReportWriterABC):
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
all_results = prepare_results_for_writing(export_results)
|
||||
all_results = prepare_export_results_for_writing(export_results)
|
||||
for data in list(all_results.values()):
|
||||
data["report_id"] = self.report_id
|
||||
cursor = self._conn.cursor()
|
||||
@@ -284,7 +299,7 @@ class ReportWriterSQLite(ReportWriterABC):
|
||||
self.close()
|
||||
|
||||
|
||||
def prepare_results_for_writing(
|
||||
def prepare_export_results_for_writing(
|
||||
export_results: ExportResults, bool_values: bool = False
|
||||
) -> Dict:
|
||||
"""Return all results for writing to report
|
||||
@@ -406,17 +421,250 @@ def prepare_results_for_writing(
|
||||
return all_results
|
||||
|
||||
|
||||
def report_writer_factory(
|
||||
def export_report_writer_factory(
|
||||
output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
) -> ReportWriterABC:
|
||||
"""Return a ReportWriter instance appropriate for the output file type"""
|
||||
output_type = os.path.splitext(output_file)[1]
|
||||
output_type = output_type.lower()[1:]
|
||||
if output_type == "csv":
|
||||
return ReportWriterCSV(output_file, append)
|
||||
return ExportReportWriterCSV(output_file, append)
|
||||
elif output_type == "json":
|
||||
return ReportWriterJSON(output_file, append)
|
||||
return ExportReportWriterJSON(output_file, append)
|
||||
elif output_type in ["sqlite", "db"]:
|
||||
return ReportWriterSQLite(output_file, append)
|
||||
return ExportReportWriterSQLite(output_file, append)
|
||||
else:
|
||||
raise ValueError(f"Unknown report file type: {output_file}")
|
||||
|
||||
|
||||
# Classes for writing Sync results to a report file
|
||||
|
||||
|
||||
class SyncReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
mode = "a" if append else "w"
|
||||
self._output_fh = open(self.output_file, mode)
|
||||
|
||||
def write(self, sync_results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
report_columns = sync_results.results_header
|
||||
self._csv_writer = csv.DictWriter(self._output_fh, fieldnames=report_columns)
|
||||
if not self.append:
|
||||
self._csv_writer.writeheader()
|
||||
|
||||
for data in sync_results.results_list:
|
||||
self._csv_writer.writerow(dict(zip(report_columns, data)))
|
||||
self._output_fh.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._output_fh.close()
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self._output_fh.close()
|
||||
|
||||
|
||||
class SyncReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON SyncResults report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
self.indent = 4
|
||||
|
||||
self._first_record_written = False
|
||||
if append:
|
||||
with open(self.output_file, "r") as fh:
|
||||
existing_data = json.load(fh)
|
||||
self._output_fh = open(self.output_file, "w")
|
||||
self._output_fh.write("[")
|
||||
for data in existing_data:
|
||||
self._output_fh.write(json.dumps(data, indent=self.indent))
|
||||
self._output_fh.write(",\n")
|
||||
else:
|
||||
self._output_fh = open(self.output_file, "w")
|
||||
self._output_fh.write("[")
|
||||
|
||||
def write(self, results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
# convert datetimes to strings
|
||||
def default(o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
for data in list(results.results_dict.values()):
|
||||
if self._first_record_written:
|
||||
self._output_fh.write(",\n")
|
||||
else:
|
||||
self._first_record_written = True
|
||||
self._output_fh.write(json.dumps(data, indent=self.indent, default=default))
|
||||
self._output_fh.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._output_fh.write("]")
|
||||
self._output_fh.close()
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self.close()
|
||||
|
||||
|
||||
class SyncReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite SyncResults report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
|
||||
if not append:
|
||||
with suppress(FileNotFoundError):
|
||||
os.unlink(self.output_file)
|
||||
|
||||
self._conn = sqlite3.connect(self.output_file)
|
||||
self._create_tables()
|
||||
self.report_id = self._generate_report_id()
|
||||
|
||||
def write(self, results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
# insert rows of values into sqlite report table
|
||||
for row in list(results.results_list):
|
||||
report_id = self.report_id
|
||||
data = [str(v) if v else "" for v in row]
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO report "
|
||||
"(report_id, uuid, filename, fingerprint, updated, "
|
||||
"albums_updated, albums_datetime, albums_before, albums_after, "
|
||||
"description_updated, description_datetime, description_before, description_after, "
|
||||
"favorite_updated, favorite_datetime, favorite_before, favorite_after, "
|
||||
"keywords_updated, keywords_datetime, keywords_before, keywords_after, "
|
||||
"title_updated, title_datetime, title_before, title_after)"
|
||||
"VALUES "
|
||||
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(report_id, *data),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._conn.close()
|
||||
|
||||
def _create_tables(self):
|
||||
c = self._conn.cursor()
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS report (
|
||||
report_id TEXT,
|
||||
uuid TEXT,
|
||||
filename TEXT,
|
||||
fingerprint TEXT,
|
||||
updated INT,
|
||||
albums_updated INT,
|
||||
albums_datetime TEXT,
|
||||
albums_before TEXT,
|
||||
albums_after TEXT,
|
||||
description_updated INT,
|
||||
description_datetime TEXT,
|
||||
description_before TEXT,
|
||||
description_after TEXT,
|
||||
favorite_updated INT,
|
||||
favorite_datetime TEXT,
|
||||
favorite_before TEXT,
|
||||
favorite_after TEXT,
|
||||
keywords_updated INT,
|
||||
keywords_datetime TEXT,
|
||||
keywords_before TEXT,
|
||||
keywords_after TEXT,
|
||||
title_updated INT,
|
||||
title_datetime TEXT,
|
||||
title_before TEXT,
|
||||
title_after TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS about (
|
||||
id INTEGER PRIMARY KEY,
|
||||
about TEXT
|
||||
);"""
|
||||
)
|
||||
c.execute(
|
||||
"INSERT INTO about(about) VALUES (?);",
|
||||
(f"OSXPhotos Sync Report. {OSXPHOTOS_ABOUT_STRING}",),
|
||||
)
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS report_id (
|
||||
report_id INTEGER PRIMARY KEY,
|
||||
datetime TEXT
|
||||
);"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
# create report_summary view
|
||||
c.execute("DROP VIEW IF EXISTS report_summary;")
|
||||
c.execute(
|
||||
"""
|
||||
CREATE VIEW report_summary AS
|
||||
SELECT
|
||||
r.report_id,
|
||||
i.datetime AS report_datetime,
|
||||
COUNT(r.uuid) as processed,
|
||||
COUNT(CASE r.updated WHEN 'True' THEN 1 ELSE NULL END) as updated,
|
||||
COUNT(case r.albums_updated WHEN 'True' THEN 1 ELSE NULL END) as albums_updated,
|
||||
COUNT(case r.description_updated WHEN 'True' THEN 1 ELSE NULL END) as description_updated,
|
||||
COUNT(case r.favorite_updated WHEN 'True' THEN 1 ELSE NULL END) as favorite_updated,
|
||||
COUNT(case r.keywords_updated WHEN 'True' THEN 1 ELSE NULL END) as keywords_updated,
|
||||
COUNT(case r.title_updated WHEN 'True' THEN 1 ELSE NULL END) as title_updated
|
||||
FROM report as r
|
||||
INNER JOIN report_id as i ON r.report_id = i.report_id
|
||||
GROUP BY r.report_id;
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def _generate_report_id(self) -> int:
|
||||
"""Get a new report ID for this report"""
|
||||
c = self._conn.cursor()
|
||||
c.execute(
|
||||
"INSERT INTO report_id(datetime) VALUES (?);",
|
||||
(datetime.datetime.now().isoformat(),),
|
||||
)
|
||||
report_id = c.lastrowid
|
||||
self._conn.commit()
|
||||
return report_id
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self.close()
|
||||
|
||||
|
||||
def sync_report_writer_factory(
|
||||
output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
) -> ReportWriterABC:
|
||||
"""Return a ReportWriter instance appropriate for the output file type"""
|
||||
output_type = os.path.splitext(output_file)[1]
|
||||
output_type = output_type.lower()[1:]
|
||||
if output_type == "csv":
|
||||
return SyncReportWriterCSV(output_file, append)
|
||||
elif output_type == "json":
|
||||
return SyncReportWriterJSON(output_file, append)
|
||||
elif output_type in ["sqlite", "db"]:
|
||||
return SyncReportWriterSQLite(output_file, append)
|
||||
else:
|
||||
raise ValueError(f"Unknown report file type: {output_file}")
|
||||
|
||||
749
osxphotos/cli/sync.py
Normal file
749
osxphotos/cli/sync.py
Normal file
@@ -0,0 +1,749 @@
|
||||
"""Sync metadata and albums between Photos libraries"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB, __version__
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.photosdb.photosdb_utils import get_db_version
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import sync_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .sync_results import SYNC_PROPERTIES, SyncResults
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
SYNC_ABOUT_STRING = (
|
||||
f"Sync Metadata Database created by osxphotos version {__version__} "
|
||||
+ f"(https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||
)
|
||||
|
||||
SYNC_IMPORT_TYPES = [
|
||||
"keywords",
|
||||
"albums",
|
||||
"title",
|
||||
"description",
|
||||
"favorite",
|
||||
]
|
||||
SYNC_IMPORT_TYPES_ALL = ["all"] + SYNC_IMPORT_TYPES
|
||||
|
||||
|
||||
class SyncImportPath(click.ParamType):
|
||||
"""A path to a Photos library or a metadata export file created by --export"""
|
||||
|
||||
name = "SYNC_IMPORT_PATH"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
if not pathlib.Path(value).exists():
|
||||
self.fail(f"{value} is not a file or directory")
|
||||
value = str(pathlib.Path(value).expanduser().resolve())
|
||||
# call get_import_type to raise exception if not a valid import type
|
||||
get_import_type(value)
|
||||
return value
|
||||
except Exception as e:
|
||||
self.fail(f"Could not determine import type for {value}: {e}")
|
||||
|
||||
|
||||
class SyncImportType(click.ParamType):
|
||||
"""A string indicating which metadata to set or merge from the import source"""
|
||||
|
||||
# valid values are specified in METADATA_IMPORT_TYPES_ALL
|
||||
|
||||
name = "SYNC_IMPORT_TYPE"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
if value not in SYNC_IMPORT_TYPES_ALL:
|
||||
values = [v.strip() for v in value.split(",")]
|
||||
for v in values:
|
||||
if v not in SYNC_IMPORT_TYPES_ALL:
|
||||
self.fail(
|
||||
f"{v} is not a valid import type, valid values are {', '.join(SYNC_IMPORT_TYPES_ALL)}"
|
||||
)
|
||||
return value
|
||||
except Exception as e:
|
||||
self.fail(f"Could not determine import type for {value}: {e}")
|
||||
|
||||
|
||||
def render_and_validate_report(report: str) -> str:
|
||||
"""Render a report file template and validate the filename
|
||||
|
||||
Args:
|
||||
report: the template string
|
||||
|
||||
Returns:
|
||||
the rendered report filename
|
||||
|
||||
Note:
|
||||
Exits with error if the report filename is invalid
|
||||
"""
|
||||
# render report template and validate the filename
|
||||
template = PhotoTemplate(PhotoInfoNone())
|
||||
render_options = RenderOptions()
|
||||
report_file, _ = template.render(report, options=render_options)
|
||||
report = report_file[0]
|
||||
|
||||
if os.path.isdir(report):
|
||||
rich_click_echo(
|
||||
f"[error]Report '{report}' is a directory, must be file name",
|
||||
err=True,
|
||||
)
|
||||
raise click.Abort()
|
||||
return report
|
||||
|
||||
|
||||
def parse_set_merge(values: tuple[str]) -> tuple[str]:
|
||||
"""Parse --set and --merge options which may be passed individually or as a comma-separated list"""
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.extend([v.strip() for v in value.split(",")])
|
||||
return tuple(new_values)
|
||||
|
||||
|
||||
def open_metadata_db(db_path: str):
|
||||
"""Open metadata database at db_path"""
|
||||
metadata_db = SQLiteKVStore(
|
||||
db_path,
|
||||
wal=False, # don't use WAL to keep database a single file
|
||||
)
|
||||
if not metadata_db.about:
|
||||
metadata_db.about = f"osxphotos metadata sync database\n{SYNC_ABOUT_STRING}"
|
||||
return metadata_db
|
||||
|
||||
|
||||
def key_from_photo(photo: PhotoInfo) -> str:
|
||||
"""Return key for photo used to correlate photos between libraries"""
|
||||
return f"{photo.fingerprint}:{photo.original_filename}"
|
||||
|
||||
|
||||
def get_photo_metadata(photos: list[PhotoInfo]) -> str:
|
||||
"""Return JSON string of metadata for photos; if more than one photo, merge metadata"""
|
||||
if len(photos) == 1:
|
||||
return photos[0].json()
|
||||
|
||||
# more than one photo with same fingerprint; merge metadata
|
||||
merge_fields = ["keywords", "persons", "albums", "title", "description", "uuid"]
|
||||
photos_dict = {}
|
||||
for photo in photos:
|
||||
data = photo.asdict()
|
||||
for k, v in data.items():
|
||||
if k not in photos_dict:
|
||||
photos_dict[k] = v.copy() if isinstance(v, (list, dict)) else v
|
||||
else:
|
||||
# merge data if it's a merge field
|
||||
if k in merge_fields and v:
|
||||
if isinstance(v, (list, tuple)):
|
||||
photos_dict[k] = sorted(list(set(photos_dict[k]) | set(v)))
|
||||
else:
|
||||
if v:
|
||||
if not photos_dict[k]:
|
||||
photos_dict[k] = v
|
||||
elif photos_dict[k] and v != photos_dict[k]:
|
||||
photos_dict[k] = f"{photos_dict[k]} {v}"
|
||||
# convert photos_dict to JSON string
|
||||
# wouldn't it be nice if json encoder handled datetimes...
|
||||
def default(o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
return json.dumps(photos_dict, sort_keys=True, default=default)
|
||||
|
||||
|
||||
def export_metadata(
|
||||
photos: list[PhotoInfo], output_path: str, verbose: Callable[..., None]
|
||||
):
|
||||
"""Export metadata to metadata_db"""
|
||||
metadata_db = open_metadata_db(output_path)
|
||||
verbose(f"Exporting metadata to [filepath]{output_path}[/]")
|
||||
num_photos = len(photos)
|
||||
photo_word = pluralize(num_photos, "photo", "photos")
|
||||
verbose(f"Analyzing [num]{num_photos}[/] {photo_word} to export")
|
||||
verbose(f"Exporting [num]{len(photos)}[/] {photo_word} to {output_path}")
|
||||
export_metadata_to_db(photos, metadata_db, progress=True)
|
||||
rich_click_echo(
|
||||
f"Done: exported metadata for [num]{len(photos)}[/] {photo_word} to [filepath]{output_path}[/]"
|
||||
)
|
||||
metadata_db.close()
|
||||
|
||||
|
||||
def export_metadata_to_db(
|
||||
photos: list[PhotoInfo],
|
||||
metadata_db: SQLiteKVStore,
|
||||
progress: bool = True,
|
||||
):
|
||||
"""Export metadata for photos to metadata database
|
||||
|
||||
Args:
|
||||
photos: list of PhotoInfo objects
|
||||
metadata_db: SQLiteKVStore object
|
||||
progress: if True, show progress bar
|
||||
"""
|
||||
# it is possible to have multiple photos with the same fingerprint
|
||||
# for example, the same photo was imported twice or the photo was duplicated in Photos
|
||||
# in this case, we need to merge the metadata for the photos with the same fingerprint
|
||||
# as there is no way to know which photo is the "correct" one
|
||||
key_to_photos = {}
|
||||
for photo in photos:
|
||||
key = key_from_photo(photo)
|
||||
if key in key_to_photos:
|
||||
key_to_photos[key].append(photo)
|
||||
else:
|
||||
key_to_photos[key] = [photo]
|
||||
|
||||
with rich_progress(console=get_verbose_console(), mock=not progress) as progress:
|
||||
task = progress.add_task("Exporting metadata", total=len(key_to_photos))
|
||||
for key, key_photos in key_to_photos.items():
|
||||
metadata_db[key] = get_photo_metadata(key_photos)
|
||||
progress.advance(task)
|
||||
|
||||
|
||||
def get_import_type(import_path: str) -> Literal["library", "export"]:
|
||||
"""Determine if import_path is a Photos library, Photos database, or metadata export file"""
|
||||
if pathlib.Path(import_path).is_dir():
|
||||
if import_path.endswith(".photoslibrary"):
|
||||
return "library"
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import library: {import_path}"
|
||||
)
|
||||
else:
|
||||
# import_path is a file, need to determine if it's a Photos database or metadata export file
|
||||
try:
|
||||
get_db_version(import_path)
|
||||
except Exception as e:
|
||||
try:
|
||||
db = SQLiteKVStore(import_path)
|
||||
if db.about:
|
||||
return "export"
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import file: {import_path}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import file: {import_path}"
|
||||
) from e
|
||||
else:
|
||||
return "library"
|
||||
|
||||
|
||||
def import_metadata(
|
||||
photos: list[PhotoInfo],
|
||||
import_path: str,
|
||||
set_: tuple[str, ...],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Import metadata from metadata_db"""
|
||||
import_type = get_import_type(import_path)
|
||||
photo_word = pluralize(len(photos), "photo", "photos")
|
||||
verbose(
|
||||
f"Importing metadata for [num]{len(photos)}[/] {photo_word} from [filepath]{import_path}[/]"
|
||||
)
|
||||
|
||||
# build mapping of key to photo
|
||||
key_to_photo = {}
|
||||
for photo in photos:
|
||||
key = key_from_photo(photo)
|
||||
if key in key_to_photo:
|
||||
key_to_photo[key].append(photo)
|
||||
else:
|
||||
key_to_photo[key] = [photo]
|
||||
|
||||
# find keys in import_path that match keys in photos
|
||||
if import_type == "library":
|
||||
# create an in memory database of the import library
|
||||
# so that the rest of the comparison code can be the same
|
||||
photosdb = PhotosDB(import_path, verbose=verbose)
|
||||
photos = photosdb.photos()
|
||||
import_db = SQLiteKVStore(":memory:")
|
||||
verbose(f"Loading metadata from import library: [filepath]{import_path}[/]")
|
||||
export_metadata_to_db(photos, import_db, progress=False)
|
||||
elif import_type == "export":
|
||||
import_db = open_metadata_db(import_path)
|
||||
else:
|
||||
rich_echo_error(
|
||||
f"Unable to determine type of import file: [filepath]{import_path}[/]"
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
results = SyncResults()
|
||||
for key, key_photos in key_to_photo.items():
|
||||
if key in import_db:
|
||||
# import metadata from import_db
|
||||
for photo in key_photos:
|
||||
rich_click_echo(
|
||||
f"Importing metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
metadata = import_db[key]
|
||||
results += import_metadata_for_photo(
|
||||
photo, metadata, set_, merge, dry_run, verbose
|
||||
)
|
||||
else:
|
||||
# unable to find metadata for photo in import_db
|
||||
for photo in key_photos:
|
||||
rich_click_echo(
|
||||
f"Unable to find metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/]) in [filepath]{import_path}[/]"
|
||||
)
|
||||
|
||||
# find any keys in import_db that don't match keys in photos
|
||||
for key in import_db.keys():
|
||||
if key not in key_to_photo:
|
||||
rich_click_echo(f"Unable to find [uuid]{key}[/] in current library.")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def import_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: str,
|
||||
set_: tuple[str, ...],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Update metadata for photo from metadata
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo object
|
||||
metadata: metadata to import (JSON string)
|
||||
set_: tuple of metadata fields to set
|
||||
merge: tuple of metadata fields to merge
|
||||
dry_run: if True, don't actually update metadata
|
||||
verbose: verbose function
|
||||
"""
|
||||
# convert metadata to dict
|
||||
metadata = json.loads(metadata)
|
||||
|
||||
results = SyncResults()
|
||||
if "albums" in set_ or "albums" in merge:
|
||||
# behavior is the same for albums for set and merge:
|
||||
# add photo to any new albums but do not remove from existing albums
|
||||
results += _update_albums_for_photo(photo, metadata, dry_run, verbose)
|
||||
|
||||
results += _set_metadata_for_photo(photo, metadata, set_, dry_run, verbose)
|
||||
results += _merge_metadata_for_photo(photo, metadata, merge, dry_run, verbose)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _update_albums_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Add photo to new albums if necessary"""
|
||||
# add photo to any new albums but do not remove from existing albums
|
||||
results = SyncResults()
|
||||
value = sorted(metadata["albums"])
|
||||
before = sorted(photo.albums)
|
||||
albums_to_add = set(value) - set(before)
|
||||
if not albums_to_add:
|
||||
verbose(f"\tNothing to do for albums")
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
"albums",
|
||||
False,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
for album in albums_to_add:
|
||||
verbose(f"\tAdding to album [filepath]{album}[/]")
|
||||
if not dry_run:
|
||||
PhotosAlbum(album, verbose=lambda x: verbose(f"\t{x}"), rich=True).add(
|
||||
photo
|
||||
)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
"albums",
|
||||
True,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _set_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
set_: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Set metadata for photo"""
|
||||
results = SyncResults()
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
|
||||
for field in set_:
|
||||
if field == "albums":
|
||||
continue
|
||||
|
||||
value = metadata[field]
|
||||
before = getattr(photo, field)
|
||||
|
||||
if isinstance(value, list):
|
||||
value = sorted(value)
|
||||
if isinstance(before, list):
|
||||
before = sorted(before)
|
||||
|
||||
if value != before:
|
||||
verbose(f"\tSetting {field} to {value} from {before}")
|
||||
if not dry_run:
|
||||
set_photo_property(photo_, field, value)
|
||||
else:
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
value != before,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _merge_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Merge metadata for photo"""
|
||||
results = SyncResults()
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
|
||||
for field in merge:
|
||||
if field == "albums":
|
||||
continue
|
||||
|
||||
value = metadata[field]
|
||||
before = getattr(photo, field)
|
||||
|
||||
if isinstance(value, list):
|
||||
value = sorted(value)
|
||||
if isinstance(before, list):
|
||||
before = sorted(before)
|
||||
|
||||
if value == before:
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
False,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(value, list) and isinstance(before, list):
|
||||
new_value = sorted(set(value + before))
|
||||
elif isinstance(before, bool):
|
||||
new_value = value or bool(before)
|
||||
elif isinstance(before, str):
|
||||
value = value or ""
|
||||
new_value = f"{before} {value}" if value and value not in before else before
|
||||
elif before is None:
|
||||
new_value = value
|
||||
else:
|
||||
rich_echo_error(
|
||||
f"Unable to merge {field} for [filename]{photo.original_filename}[filename]"
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
if new_value != before:
|
||||
verbose(f"\tMerging {field} to {new_value} from {before}")
|
||||
if not dry_run:
|
||||
set_photo_property(photo_, field, new_value)
|
||||
else:
|
||||
# Merge'd value might still be the same as original value
|
||||
# (e.g. if value is str and has previously been merged)
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
new_value != before,
|
||||
before,
|
||||
new_value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def set_photo_property(photo: photoscript.Photo, property: str, value: Any):
|
||||
"""Set property on photo"""
|
||||
|
||||
# do some basic validation
|
||||
if property == "keywords" and not isinstance(value, list):
|
||||
raise ValueError(f"keywords must be a list, not {type(value)}")
|
||||
elif property in {"title", "description"} and not isinstance(value, str):
|
||||
raise ValueError(f"{property} must be a str, not {type(value)}")
|
||||
elif property == "favorite":
|
||||
value = bool(value)
|
||||
elif property not in {"title", "description", "favorite", "keywords"}:
|
||||
raise ValueError(f"Unknown property: {property}")
|
||||
setattr(photo, property, value)
|
||||
|
||||
|
||||
def print_import_summary(results: SyncResults):
|
||||
"""Print summary of import results"""
|
||||
summary = results.results_summary()
|
||||
property_summary = ", ".join(
|
||||
f"updated {property}: [num]{summary.get(property,0)}[/]"
|
||||
for property in SYNC_PROPERTIES
|
||||
)
|
||||
rich_click_echo(
|
||||
f"Processed [num]{summary['total']}[/] photos, updated: [num]{summary['updated']}[/], {property_summary}"
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--export",
|
||||
"-e",
|
||||
"export_path",
|
||||
metavar="EXPORT_FILE",
|
||||
help="Export metadata to file EXPORT_FILE for later use with --import. "
|
||||
"The export file will be a SQLite database; it is recommended to use the "
|
||||
".db extension though this is not required.",
|
||||
type=click.Path(dir_okay=False, writable=True),
|
||||
)
|
||||
@click.option(
|
||||
"--import",
|
||||
"-i",
|
||||
"import_path",
|
||||
metavar="IMPORT_PATH",
|
||||
help="Import metadata from file IMPORT_PATH. "
|
||||
"IMPORT_PATH can a Photos library, a Photos database, or a metadata export file "
|
||||
"created with --export.",
|
||||
type=SyncImportPath(),
|
||||
)
|
||||
@click.option(
|
||||
"--set",
|
||||
"-s",
|
||||
"set_",
|
||||
metavar="METADATA",
|
||||
multiple=True,
|
||||
help="When used with --import, set metadata in local Photos library to match import data. "
|
||||
"Multiple metadata properties can be specified by repeating the --set option "
|
||||
"or by using a comma-separated list. "
|
||||
f"METADATA can be one of: {', '.join(SYNC_IMPORT_TYPES_ALL)}. "
|
||||
"For example, to set keywords and favorite, use `--set keywords --set favorite` "
|
||||
"or `--set keywords,favorite`. "
|
||||
"If `--set all` is specified, all metadata will be set. "
|
||||
"Note that using --set overwrites any existing metadata in the local Photos library. "
|
||||
"For example, if a photo is marked as favorite in the local library but not in the import source, "
|
||||
"--set favorite will clear the favorite status in the local library. "
|
||||
"The exception to this is that `--set album` will not remove the photo "
|
||||
"from any existing albums in the local library but will add the photo to any new albums specified "
|
||||
"in the import source."
|
||||
"See also --merge.",
|
||||
type=SyncImportType(),
|
||||
)
|
||||
@click.option(
|
||||
"--merge",
|
||||
"-m",
|
||||
"merge",
|
||||
metavar="METADATA",
|
||||
multiple=True,
|
||||
help="When used with --import, merge metadata in local Photos library with import data. "
|
||||
"Multiple metadata properties can be specified by repeating the --merge option "
|
||||
"or by using a comma-separated list. "
|
||||
f"METADATA can be one of: {', '.join(SYNC_IMPORT_TYPES_ALL)}. "
|
||||
"For example, to merge keywords and favorite, use `--merge keywords --merge favorite` "
|
||||
"or `--merge keywords,favorite`. "
|
||||
"If `--merge all` is specified, all metadata will be merged. "
|
||||
"Note that using --merge does not overwrite any existing metadata in the local Photos library. "
|
||||
"For example, if a photo is marked as favorite in the local library but not in the import source, "
|
||||
"--merge favorite will not change the favorite status in the local library. "
|
||||
"See also --set.",
|
||||
type=SyncImportType(),
|
||||
)
|
||||
@click.option(
|
||||
"--report",
|
||||
"-R",
|
||||
metavar="REPORT_FILE",
|
||||
help="Write a report of all photos that were processed with --import. "
|
||||
"The extension of the report filename will be used to determine the format. "
|
||||
"Valid extensions are: "
|
||||
".csv (CSV file), .json (JSON), .db and .sqlite (SQLite database). "
|
||||
"REPORT_FILE may be a an osxphotos template string, for example, "
|
||||
"--report 'update_{today.date}.csv' will write a CSV report file named with today's date. "
|
||||
"See also --append.",
|
||||
type=TemplateString(),
|
||||
)
|
||||
@click.option(
|
||||
"--append",
|
||||
"-A",
|
||||
is_flag=True,
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
help="Dry run; " "when used with --import, don't actually update metadata.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
|
||||
)
|
||||
@QUERY_OPTIONS
|
||||
@DB_OPTION
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def sync(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
append,
|
||||
dry_run,
|
||||
export_path,
|
||||
import_path,
|
||||
merge,
|
||||
report,
|
||||
set_,
|
||||
theme,
|
||||
timestamp,
|
||||
verbose_,
|
||||
**kwargs, # query options
|
||||
):
|
||||
"""Sync metadata and albums between Photos libraries.
|
||||
|
||||
Use sync to update metadata in a local Photos library to match
|
||||
metadata in another Photos library. The sync command works by
|
||||
finding identical photos in the local library and the import source
|
||||
and then updating the metadata in the local library to match the
|
||||
metadata in the import source. Photos are considered identical if
|
||||
their original filename and fingerprint match.
|
||||
|
||||
The import source can be a Photos library or a metadata export file
|
||||
created with the --export option.
|
||||
|
||||
The sync command can be useful if you have imported the same photos to
|
||||
multiple Photos libraries and want to keep the metadata in all libraries
|
||||
in sync.
|
||||
|
||||
Metadata can be overwritten (--set) or merged (--merge) with the metadata
|
||||
in the import source. You may specify specific metadata to sync or sync
|
||||
all metadata. See --set and --merge for more details.
|
||||
|
||||
The sync command can be used to sync metadata between an iPhone or iPad
|
||||
and a Mac, for example, in the case where you do not use iCloud but
|
||||
manually import photos from your iPhone or iPad to your Mac. To do this,
|
||||
you'll first need to copy the Photos database from the iPhone or iPad to
|
||||
your Mac. This can be done by connecting your iPhone or iPad to your Mac
|
||||
using a USB cable and then copying the Photos database from the iPhone
|
||||
using a third-party tool such as iMazing (https://imazing.com/). You can
|
||||
then use the sync command and set the import source to the Photos database
|
||||
you copied from the iPhone or iPad.
|
||||
|
||||
The sync command can also be used to sync metadata between users using
|
||||
iCloud Shared Photo Library. NOTE: This use case has not yet been
|
||||
tested. If you use iCloud Shared Photo Library and would like to help
|
||||
test this use case, please connect with me on GitHub:
|
||||
https://github.com/RhetTbull/osxphotos/issues/887
|
||||
|
||||
You can run the --export and --import commands together. In this case,
|
||||
the import will be run first and then the export will be run.
|
||||
|
||||
For example, if you want to sync two Photos libraries between users or
|
||||
two different computers, you can export the metadata to a shared folder.
|
||||
|
||||
On the first computer, run:
|
||||
|
||||
osxphotos sync --export /path/to/export/folder/computer1.db --merge all --import /path/to/export/folder/computer2.db
|
||||
|
||||
On the second computer, run:
|
||||
|
||||
osxphotos sync --export /path/to/export/folder/computer2.db --merge all --import /path/to/export/folder/computer1.db
|
||||
|
||||
"""
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
if (set_ or merge) and not import_path:
|
||||
rich_echo_error("--set and --merge can only be used with --import")
|
||||
ctx.exit(1)
|
||||
|
||||
set_ = parse_set_merge(set_)
|
||||
merge = parse_set_merge(merge)
|
||||
|
||||
if "all" in set_:
|
||||
set_ = tuple(SYNC_IMPORT_TYPES)
|
||||
if "all" in merge:
|
||||
merge = tuple(SYNC_IMPORT_TYPES)
|
||||
|
||||
if set_ and merge:
|
||||
# fields in set cannot be in merge and vice versa
|
||||
set_ = set(set_)
|
||||
merge = set(merge)
|
||||
if set_ & merge:
|
||||
rich_echo_error(
|
||||
"--set and --merge cannot be used with the same fields: "
|
||||
f"set: {set_}, merge: {merge}"
|
||||
)
|
||||
ctx.exit(1)
|
||||
|
||||
if import_path:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
photos = photosdb.query(query_options)
|
||||
results = import_metadata(photos, import_path, set_, merge, dry_run, verbose)
|
||||
if report:
|
||||
report_path = render_and_validate_report(report)
|
||||
verbose(f"Writing report to {report_path}")
|
||||
report_writer = sync_report_writer_factory(report_path, append=append)
|
||||
report_writer.write(results)
|
||||
report_writer.close()
|
||||
print_import_summary(results)
|
||||
|
||||
if export_path:
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
photos = photosdb.query(query_options)
|
||||
export_metadata(photos, export_path, verbose)
|
||||
175
osxphotos/cli/sync_results.py
Normal file
175
osxphotos/cli/sync_results.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""SyncResults class for osxphotos sync command"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from osxphotos.photoinfo import PhotoInfo
|
||||
|
||||
SYNC_PROPERTIES = [
|
||||
"albums",
|
||||
"description",
|
||||
"favorite",
|
||||
"keywords",
|
||||
"title",
|
||||
]
|
||||
|
||||
|
||||
class SyncResults:
|
||||
"""Results of sync set/merge"""
|
||||
|
||||
def __init__(self):
|
||||
self._results = {}
|
||||
self._datetime = datetime.datetime.now()
|
||||
|
||||
def add_result(
|
||||
self,
|
||||
uuid: str,
|
||||
filename: str,
|
||||
fingerprint: str,
|
||||
property: str,
|
||||
updated: bool,
|
||||
before: str | list[str] | bool | None,
|
||||
after: str | list[str] | bool | None,
|
||||
):
|
||||
"""Add result for a single photo"""
|
||||
if uuid not in self._results:
|
||||
self._results[uuid] = {
|
||||
"filename": filename,
|
||||
"fingerprint": fingerprint,
|
||||
"properties": {
|
||||
property: {
|
||||
"updated": updated,
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"before": before,
|
||||
"after": after,
|
||||
},
|
||||
},
|
||||
}
|
||||
else:
|
||||
self._results[uuid]["properties"][property] = {
|
||||
"updated": updated,
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
"""Return results"""
|
||||
return self._results
|
||||
|
||||
@property
|
||||
def results_list(self):
|
||||
"""Return results as list lists where each sublist is values for a single photo"""
|
||||
results = []
|
||||
for uuid, record in self._results.items():
|
||||
row = [
|
||||
uuid,
|
||||
record["filename"],
|
||||
record["fingerprint"],
|
||||
self._any_updated(uuid),
|
||||
]
|
||||
for property in SYNC_PROPERTIES:
|
||||
if property in record["properties"]:
|
||||
row.extend(
|
||||
record["properties"][property][column]
|
||||
for column in ["updated", "datetime", "before", "after"]
|
||||
)
|
||||
else:
|
||||
row.extend([False, "", "", ""])
|
||||
results.append(row)
|
||||
return results
|
||||
|
||||
@property
|
||||
def results_header(self):
|
||||
"""Return headers for results_list"""
|
||||
header = ["uuid", "filename", "fingerprint", "updated"]
|
||||
for property in SYNC_PROPERTIES:
|
||||
header.extend(
|
||||
f"{property}_{column}"
|
||||
for column in ["updated", "datetime", "before", "after"]
|
||||
)
|
||||
return header
|
||||
|
||||
@property
|
||||
def results_dict(self):
|
||||
"""Return dictionary of results"""
|
||||
results = {}
|
||||
for uuid, record in self._results.items():
|
||||
results[uuid] = {
|
||||
"uuid": uuid,
|
||||
"filename": record["filename"],
|
||||
"fingerprint": record["fingerprint"],
|
||||
"updated": self._any_updated(uuid),
|
||||
}
|
||||
for property in SYNC_PROPERTIES:
|
||||
if property in record["properties"]:
|
||||
results[uuid][property] = record["properties"][property]
|
||||
else:
|
||||
results[uuid][property] = {
|
||||
"updated": False,
|
||||
"datetime": None,
|
||||
"before": None,
|
||||
"after": None,
|
||||
}
|
||||
return results
|
||||
|
||||
def results_summary(self):
|
||||
"""Get summary of results"""
|
||||
updated = sum(bool(self._any_updated(uuid)) for uuid in self._results.keys())
|
||||
property_updated = {}
|
||||
for property in SYNC_PROPERTIES:
|
||||
property_updated[property] = 0
|
||||
for uuid in self._results.keys():
|
||||
if self._results[uuid]["properties"].get(property, {"updated": False})[
|
||||
"updated"
|
||||
]:
|
||||
property_updated[property] += 1
|
||||
return {
|
||||
"total": len(self._results),
|
||||
"updated": updated,
|
||||
} | property_updated
|
||||
|
||||
def _any_updated(self, uuid: str) -> bool:
|
||||
"""Return True if any property was updated for this photo"""
|
||||
return any(
|
||||
self._results[uuid]["properties"].get(property, {"updated": False})[
|
||||
"updated"
|
||||
]
|
||||
for property in SYNC_PROPERTIES
|
||||
)
|
||||
|
||||
def __add__(self, other):
|
||||
"""Add results from another SyncResults"""
|
||||
for uuid in other._results.keys():
|
||||
for property, values in other._results[uuid]["properties"].items():
|
||||
self.add_result(
|
||||
uuid,
|
||||
other._results[uuid]["filename"],
|
||||
other._results[uuid]["fingerprint"],
|
||||
property,
|
||||
values["updated"],
|
||||
values["before"],
|
||||
values["after"],
|
||||
)
|
||||
return self
|
||||
|
||||
def __iadd__(self, other):
|
||||
"""Add results from another SyncResults"""
|
||||
for uuid in other._results.keys():
|
||||
for property, values in other._results[uuid]["properties"].items():
|
||||
self.add_result(
|
||||
uuid,
|
||||
other._results[uuid]["filename"],
|
||||
other._results[uuid]["fingerprint"],
|
||||
property,
|
||||
values["updated"],
|
||||
values["before"],
|
||||
values["after"],
|
||||
)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self._results, indent=2)
|
||||
@@ -49,7 +49,7 @@ def debug_breakpoint(wrapped, instance, args, kwargs):
|
||||
|
||||
def wrap_function(function_path, wrapper):
|
||||
"""Wrap a function with wrapper function"""
|
||||
module, name = function_path.split(".", 1)
|
||||
module, name = function_path.split("::", 1)
|
||||
try:
|
||||
return wrapt.wrap_function_wrapper(module, name, wrapper)
|
||||
except AttributeError as e:
|
||||
|
||||
Binary file not shown.
@@ -41,6 +41,11 @@ ExifDateTime = namedtuple(
|
||||
|
||||
def exif_offset_to_seconds(offset: str) -> int:
|
||||
"""Convert timezone offset from UTC in exiftool format (+/-hh:mm) to seconds"""
|
||||
|
||||
# Z (for Zulu time) corresponds to a zero UTC offset
|
||||
if offset == "Z":
|
||||
return 0
|
||||
|
||||
sign = 1 if offset[0] == "+" else -1
|
||||
hours, minutes = offset[1:].split(":")
|
||||
return sign * (int(hours) * 3600 + int(minutes) * 60)
|
||||
@@ -280,6 +285,7 @@ def get_exif_date_time_offset(
|
||||
time_fields = [
|
||||
"EXIF:DateTimeOriginal",
|
||||
"EXIF:CreateDate",
|
||||
"QuickTime:ContentCreateDate",
|
||||
"QuickTime:CreationDate",
|
||||
"QuickTime:CreateDate",
|
||||
"IPTC:DateCreated",
|
||||
|
||||
@@ -123,6 +123,7 @@ class _ExifToolProc:
|
||||
)
|
||||
return
|
||||
self._process_running = False
|
||||
self._large_file_support = large_file_support
|
||||
self._exiftool = exiftool or get_exiftool_path()
|
||||
self._start_proc(large_file_support=large_file_support)
|
||||
|
||||
@@ -130,7 +131,7 @@ class _ExifToolProc:
|
||||
def process(self):
|
||||
"""return the exiftool subprocess"""
|
||||
if not self._process_running:
|
||||
self._start_proc()
|
||||
self._start_proc(large_file_support=self._large_file_support)
|
||||
return self._process
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
""" Helper class for managing database used by PhotoExporter for tracking state of exports and updates """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
@@ -32,7 +32,7 @@ __all__ = [
|
||||
"ExportDBTemp",
|
||||
]
|
||||
|
||||
OSXPHOTOS_EXPORTDB_VERSION = "7.1"
|
||||
OSXPHOTOS_EXPORTDB_VERSION = "8.0"
|
||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||
|
||||
# max retry attempts for methods which use tenacity.retry
|
||||
@@ -532,6 +532,10 @@ class ExportDB:
|
||||
# add timestamp to export_data
|
||||
self._migrate_7_0_to_7_1(conn)
|
||||
|
||||
if version[1] < "8.0":
|
||||
# add error to export_data
|
||||
self._migrate_7_1_to_8_0(conn)
|
||||
|
||||
conn.execute("VACUUM;")
|
||||
conn.commit()
|
||||
|
||||
@@ -739,6 +743,7 @@ class ExportDB:
|
||||
conn.commit()
|
||||
|
||||
def _migrate_7_0_to_7_1(self, conn):
|
||||
"""Add timestamp column to export_data table and triggers to update it on insert and update."""
|
||||
c = conn.cursor()
|
||||
# timestamp column should not exist but this prevents error if migration is run on an already migrated database
|
||||
# reference #794
|
||||
@@ -767,6 +772,16 @@ class ExportDB:
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _migrate_7_1_to_8_0(self, conn):
|
||||
"""Add error column to export_data table"""
|
||||
c = conn.cursor()
|
||||
results = c.execute(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('export_data') WHERE name='error';"
|
||||
).fetchone()
|
||||
if results[0] == 0:
|
||||
c.execute("""ALTER TABLE export_data ADD COLUMN error JSON;""")
|
||||
conn.commit()
|
||||
|
||||
def _perform_db_maintenace(self, conn):
|
||||
"""Perform database maintenance"""
|
||||
c = conn.cursor()
|
||||
@@ -1133,6 +1148,35 @@ class ExportRecord:
|
||||
f"No timestamp found in database for {self._filepath_normalized}"
|
||||
)
|
||||
|
||||
@property
|
||||
def error(self) -> dict[str, Any] | None:
|
||||
"""Return error value"""
|
||||
conn = self._conn
|
||||
c = conn.cursor()
|
||||
if row := c.execute(
|
||||
"SELECT error FROM export_data WHERE filepath_normalized = ?;",
|
||||
(self._filepath_normalized,),
|
||||
).fetchone():
|
||||
return json.loads(row[0]) if row[0] else None
|
||||
|
||||
raise ValueError(f"No error found in database for {self._filepath_normalized}")
|
||||
|
||||
@error.setter
|
||||
def error(self, value: dict[str, Any] | None):
|
||||
"""Set error value"""
|
||||
conn = self._conn
|
||||
c = conn.cursor()
|
||||
if value is None:
|
||||
value = ""
|
||||
# use default=str because some of the values are Path objects
|
||||
error = json.dumps(value, default=str)
|
||||
c.execute(
|
||||
"UPDATE export_data SET error = ? WHERE filepath_normalized = ?;",
|
||||
(error, self._filepath_normalized),
|
||||
)
|
||||
if not self._context_manager:
|
||||
conn.commit()
|
||||
|
||||
def asdict(self):
|
||||
"""Return dict of self"""
|
||||
exifdata = json.loads(self.exifdata) if self.exifdata else None
|
||||
@@ -1147,6 +1191,7 @@ class ExportRecord:
|
||||
"dest_sig": self.dest_sig,
|
||||
"export_options": self.export_options,
|
||||
"exifdata": exifdata,
|
||||
"error": self.error,
|
||||
"photoinfo": photoinfo,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .utils import noop
|
||||
|
||||
__all__ = [
|
||||
"export_db_check_signatures",
|
||||
"export_db_get_errors",
|
||||
"export_db_get_last_run",
|
||||
"export_db_get_version",
|
||||
"export_db_save_config_to_file",
|
||||
@@ -40,10 +41,9 @@ def export_db_get_version(
|
||||
"""returns version from export database as tuple of (osxphotos version, export_db version)"""
|
||||
conn = sqlite3.connect(str(dbfile))
|
||||
c = conn.cursor()
|
||||
row = c.execute(
|
||||
if row := c.execute(
|
||||
"SELECT osxphotos, exportdb FROM version ORDER BY id DESC LIMIT 1;"
|
||||
).fetchone()
|
||||
if row:
|
||||
).fetchone():
|
||||
return (row[0], row[1])
|
||||
return (None, None)
|
||||
|
||||
@@ -80,11 +80,13 @@ def export_db_update_signatures(
|
||||
filepath = export_dir / filepath
|
||||
if not os.path.exists(filepath):
|
||||
skipped += 1
|
||||
verbose_(f"[dark_orange]Skipping missing file[/dark_orange]: '{filepath}'")
|
||||
verbose_(
|
||||
f"[dark_orange]Skipping missing file[/dark_orange]: '[filepath]{filepath}[/]'"
|
||||
)
|
||||
continue
|
||||
updated += 1
|
||||
file_sig = fileutil.file_sig(filepath)
|
||||
verbose_(f"[green]Updating signature for[/green]: '{filepath}'")
|
||||
verbose_(f"[green]Updating signature for[/green]: '[filepath]{filepath}[/]'")
|
||||
if not dry_run:
|
||||
c.execute(
|
||||
"UPDATE export_data SET dest_mode = ?, dest_size = ?, dest_mtime = ? WHERE filepath_normalized = ?;",
|
||||
@@ -103,14 +105,29 @@ def export_db_get_last_run(
|
||||
"""Get last run from export database"""
|
||||
conn = sqlite3.connect(str(export_db))
|
||||
c = conn.cursor()
|
||||
row = c.execute(
|
||||
if row := c.execute(
|
||||
"SELECT datetime, args FROM runs ORDER BY id DESC LIMIT 1;"
|
||||
).fetchone()
|
||||
if row:
|
||||
).fetchone():
|
||||
return row[0], row[1]
|
||||
return None, None
|
||||
|
||||
|
||||
def export_db_get_errors(
|
||||
export_db: Union[str, pathlib.Path]
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Get errors from export database"""
|
||||
conn = sqlite3.connect(str(export_db))
|
||||
c = conn.cursor()
|
||||
results = c.execute(
|
||||
"SELECT filepath, uuid, timestamp, error FROM export_data WHERE error is not null ORDER BY timestamp DESC;"
|
||||
).fetchall()
|
||||
results = [
|
||||
f"[filepath]{row[0]}[/], [uuid]{row[1]}[/], [time]{row[2]}[/], [error]{row[3]}[/]"
|
||||
for row in results
|
||||
]
|
||||
return results
|
||||
|
||||
|
||||
def export_db_save_config_to_file(
|
||||
export_db: Union[str, pathlib.Path], config_file: Union[str, pathlib.Path]
|
||||
) -> None:
|
||||
@@ -138,9 +155,11 @@ def export_db_get_config(
|
||||
conn = sqlite3.connect(str(export_db))
|
||||
c = conn.cursor()
|
||||
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
|
||||
if not row:
|
||||
return ValueError("No config found in export_db")
|
||||
return config.load_from_str(row[0], override=override)
|
||||
return (
|
||||
config.load_from_str(row[0], override=override)
|
||||
if row
|
||||
else ValueError("No config found in export_db")
|
||||
)
|
||||
|
||||
|
||||
def export_db_check_signatures(
|
||||
@@ -168,16 +187,20 @@ def export_db_check_signatures(
|
||||
filepath = export_dir / filepath
|
||||
if not filepath.exists():
|
||||
skipped += 1
|
||||
verbose_(f"[dark_orange]Skipping missing file[/dark_orange]: '{filepath}'")
|
||||
verbose_(
|
||||
f"[dark_orange]Skipping missing file[/dark_orange]: '[filepath]{filepath}[/]'"
|
||||
)
|
||||
continue
|
||||
file_sig = fileutil.file_sig(filepath)
|
||||
file_rec = exportdb.get_file_record(filepath)
|
||||
if file_rec.dest_sig == file_sig:
|
||||
matched += 1
|
||||
verbose_(f"[green]Signatures matched[/green]: '{filepath}'")
|
||||
verbose_(f"[green]Signatures matched[/green]: '[filepath]{filepath}[/]'")
|
||||
else:
|
||||
notmatched += 1
|
||||
verbose_(f"[deep_pink3]Signatures do not match[/deep_pink3]: '{filepath}'")
|
||||
verbose_(
|
||||
f"[deep_pink3]Signatures do not match[/deep_pink3]: '[filepath]{filepath}[/]'"
|
||||
)
|
||||
|
||||
return (matched, notmatched, skipped)
|
||||
|
||||
@@ -196,8 +219,7 @@ def export_db_touch_files(
|
||||
|
||||
# open and close exportdb to ensure it gets migrated
|
||||
exportdb = ExportDB(dbfile, export_dir)
|
||||
upgraded = exportdb.was_upgraded
|
||||
if upgraded:
|
||||
if upgraded := exportdb.was_upgraded:
|
||||
verbose_(
|
||||
f"Upgraded export database {dbfile} from version {upgraded[0]} to {upgraded[1]}"
|
||||
)
|
||||
@@ -205,9 +227,9 @@ def export_db_touch_files(
|
||||
|
||||
conn = sqlite3.connect(str(dbfile))
|
||||
c = conn.cursor()
|
||||
# get most recent config
|
||||
row = c.execute("SELECT config FROM config ORDER BY id DESC LIMIT 1;").fetchone()
|
||||
if row:
|
||||
if row := c.execute(
|
||||
"SELECT config FROM config ORDER BY id DESC LIMIT 1;"
|
||||
).fetchone():
|
||||
config = toml.loads(row[0])
|
||||
try:
|
||||
photos_db_path = config["export"].get("db", None)
|
||||
@@ -237,7 +259,7 @@ def export_db_touch_files(
|
||||
if not filepath.exists():
|
||||
skipped += 1
|
||||
verbose_(
|
||||
f"[dark_orange]Skipping missing file (not in export directory)[/dark_orange]: '{filepath}'"
|
||||
f"[dark_orange]Skipping missing file (not in export directory)[/dark_orange]: '[filepath]{filepath}[/]'"
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -245,7 +267,7 @@ def export_db_touch_files(
|
||||
if not photo:
|
||||
skipped += 1
|
||||
verbose_(
|
||||
f"[dark_orange]Skipping missing photo (did not find in Photos Library)[/dark_orange]: '{filepath}' ({uuid})"
|
||||
f"[dark_orange]Skipping missing photo (did not find in Photos Library)[/dark_orange]: '[filepath]{filepath}[/]' ([uuid]{uuid}[/])"
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -255,14 +277,14 @@ def export_db_touch_files(
|
||||
if mtime == ts:
|
||||
not_touched += 1
|
||||
verbose_(
|
||||
f"[green]Skipping file (timestamp matches)[/green]: '{filepath}' [dodger_blue1]{isotime_from_ts(ts)} ({ts})[/dodger_blue1]"
|
||||
f"[green]Skipping file (timestamp matches)[/green]: '[filepath]{filepath}[/]' [time]{isotime_from_ts(ts)} ({ts})[/time]"
|
||||
)
|
||||
continue
|
||||
|
||||
touched += 1
|
||||
verbose_(
|
||||
f"[deep_pink3]Touching file[/deep_pink3]: '{filepath}' "
|
||||
f"[dodger_blue1]{isotime_from_ts(mtime)} ({mtime}) -> {isotime_from_ts(ts)} ({ts})[/dodger_blue1]"
|
||||
f"[deep_pink3]Touching file[/deep_pink3]: '[filepath]{filepath}[/]' "
|
||||
f"[time]{isotime_from_ts(mtime)} ({mtime}) -> {isotime_from_ts(ts)} ({ts})[/time]"
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
|
||||
@@ -76,6 +76,7 @@ class ShouldUpdate(Enum):
|
||||
EXIFTOOL_DIFFERENT = 6
|
||||
EDITED_SIG_DIFFERENT = 7
|
||||
DIGEST_DIFFERENT = 8
|
||||
UPDATE_ERRORS = 9
|
||||
|
||||
|
||||
class ExportError(Exception):
|
||||
@@ -130,6 +131,7 @@ class ExportOptions:
|
||||
timeout (int, default=120): timeout in seconds used with use_photos_export
|
||||
touch_file (bool, default=False): if True, sets file's modification time upon photo date
|
||||
update (bool, default=False): if True export will run in update mode, that is, it will not export the photo if the current version already exists in the destination
|
||||
update_errors (bool, default=False): if True photos that previously produced a warning or error will be re-exported; otherwise they will note be
|
||||
use_albums_as_keywords (bool, default = False): if True, will include album names in keywords when exporting metadata with exiftool or sidecar
|
||||
use_persons_as_keywords (bool, default = False): if True, will include person names in keywords when exporting metadata with exiftool or sidecar
|
||||
use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos even if not missing (see also use_photokit, download_missing)
|
||||
@@ -176,6 +178,7 @@ class ExportOptions:
|
||||
timeout: int = 120
|
||||
touch_file: bool = False
|
||||
update: bool = False
|
||||
update_errors: bool = False
|
||||
use_albums_as_keywords: bool = False
|
||||
use_persons_as_keywords: bool = False
|
||||
use_photokit: bool = False
|
||||
@@ -534,7 +537,12 @@ class PhotoExporter:
|
||||
)
|
||||
else:
|
||||
# guess at most likely raw name
|
||||
raw_ext = get_preferred_uti_extension(self.photo.uti_raw) or "raw"
|
||||
raw_ext = (
|
||||
get_preferred_uti_extension(self.photo.uti_raw)
|
||||
if self.photo.uti_raw
|
||||
else "raw"
|
||||
)
|
||||
raw_ext = raw_ext or "raw"
|
||||
raw_name = dest.parent / f"{dest.stem}.{raw_ext}"
|
||||
all_results.missing.append(raw_name)
|
||||
verbose(
|
||||
@@ -696,6 +704,10 @@ class PhotoExporter:
|
||||
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
||||
) -> t.Literal[True, False]:
|
||||
"""Return True if photo should be updated, else False"""
|
||||
|
||||
# NOTE: The order of certain checks is important
|
||||
# read the comments below to understand why before changing
|
||||
|
||||
export_db = options.export_db
|
||||
fileutil = options.fileutil
|
||||
|
||||
@@ -726,6 +738,14 @@ class PhotoExporter:
|
||||
# as it'll be None and bit_flags will be an int
|
||||
return ShouldUpdate.EXPORT_OPTIONS_DIFFERENT
|
||||
|
||||
if options.update_errors and file_record.error is not None:
|
||||
# files that were exported but generated an error
|
||||
# won't be updated unless --update-errors is specified
|
||||
# for example, an exiftool error due to bad metadata
|
||||
# that the user subsequently fixed should be updated; see #872
|
||||
# this must be checked before exiftool which will return False if exif data matches
|
||||
return ShouldUpdate.UPDATE_ERRORS
|
||||
|
||||
if options.exiftool:
|
||||
current_exifdata = self.exiftool_json_sidecar(options=options)
|
||||
rv = current_exifdata != file_record.exifdata
|
||||
@@ -1174,8 +1194,7 @@ class PhotoExporter:
|
||||
|
||||
# set data in the database
|
||||
with export_db.create_or_get_file_record(dest_str, self.photo.uuid) as rec:
|
||||
photoinfo = self.photo.json()
|
||||
rec.photoinfo = photoinfo
|
||||
rec.photoinfo = self.photo.json()
|
||||
rec.export_options = options.bit_flags
|
||||
# don't set src_sig as that is set above before any modifications by convert_to_jpeg or exiftool
|
||||
if not options.ignore_signature:
|
||||
@@ -1185,6 +1204,17 @@ class PhotoExporter:
|
||||
if self.photo.hexdigest != rec.digest:
|
||||
results.metadata_changed = [dest_str]
|
||||
rec.digest = self.photo.hexdigest
|
||||
# save errors to the export database (#872)
|
||||
if (
|
||||
results.error
|
||||
or exif_results.exiftool_error
|
||||
or exif_results.exiftool_warning
|
||||
):
|
||||
rec.error = {
|
||||
"error": results.error,
|
||||
"exiftool_error": exif_results.exiftool_error,
|
||||
"exiftool_warning": exif_results.exiftool_warning,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
@@ -1558,6 +1588,7 @@ class PhotoExporter:
|
||||
IPTC:DateCreated
|
||||
IPTC:TimeCreated
|
||||
QuickTime:CreationDate
|
||||
QuickTime:ContentCreateDate
|
||||
QuickTime:CreateDate (UTC)
|
||||
QuickTime:ModifyDate (UTC)
|
||||
QuickTime:GPSCoordinates
|
||||
@@ -1748,6 +1779,11 @@ class PhotoExporter:
|
||||
# https://exiftool.org/forum/index.php?topic=11927.msg64369#msg64369
|
||||
exif["QuickTime:CreationDate"] = f"{datetimeoriginal}{offsettime}"
|
||||
|
||||
# also add QuickTime:ContentCreateDate
|
||||
# reference: https://github.com/RhetTbull/osxphotos/pull/888
|
||||
# exiftool writes this field with timezone so include it here
|
||||
exif["QuickTime:ContentCreateDate"] = f"{datetimeoriginal}{offsettime}"
|
||||
|
||||
date_utc = datetime_tz_to_utc(date)
|
||||
creationdate = date_utc.strftime("%Y:%m:%d %H:%M:%S")
|
||||
exif["QuickTime:CreateDate"] = creationdate
|
||||
|
||||
@@ -4,6 +4,8 @@ Represents a single photo in the Photos library and provides access to the photo
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import datetime
|
||||
@@ -34,8 +36,11 @@ from ._constants import (
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_DERIVATIVE_PATH,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
_PHOTOS_8_SHARED_DERIVATIVE_PATH,
|
||||
_PHOTOS_8_SHARED_PHOTO_PATH,
|
||||
BURST_DEFAULT_PICK,
|
||||
BURST_KEY,
|
||||
BURST_NOT_SELECTED,
|
||||
@@ -147,38 +152,59 @@ class PhotoInfo:
|
||||
return photopath # path would be meaningless until downloaded
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return self._path_4()
|
||||
|
||||
if self._info["shared"]:
|
||||
# shared photo
|
||||
photopath = os.path.join(
|
||||
self._db._library_path,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
self._info["directory"],
|
||||
self._info["filename"],
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
photopath = None
|
||||
self._path = photopath
|
||||
return photopath
|
||||
|
||||
if self._info["directory"].startswith("/"):
|
||||
photopath = os.path.join(
|
||||
self._info["directory"], self._info["filename"]
|
||||
)
|
||||
photopath = self._path_4()
|
||||
else:
|
||||
photopath = os.path.join(
|
||||
self._db._masters_path,
|
||||
self._info["directory"],
|
||||
self._info["filename"],
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
photopath = self._path_5()
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
photopath = None
|
||||
self._path = photopath
|
||||
return photopath
|
||||
|
||||
def _path_5(self):
|
||||
"""Returns candidate path for original photo on Photos >= version 5"""
|
||||
if self._info["shared"]:
|
||||
return self._path_5_shared()
|
||||
return (
|
||||
os.path.join(self._info["directory"], self._info["filename"])
|
||||
if self._info["directory"].startswith("/")
|
||||
else os.path.join(
|
||||
self._db._masters_path,
|
||||
self._info["directory"],
|
||||
self._info["filename"],
|
||||
)
|
||||
)
|
||||
|
||||
def _path_5_shared(self):
|
||||
"""Returns candidate path for shared photo on Photos >= version 5"""
|
||||
# shared library path differs on Photos 5-7, Photos 8+
|
||||
shared_path = (
|
||||
_PHOTOS_8_SHARED_PHOTO_PATH
|
||||
if self._db._photos_ver >= 8
|
||||
else _PHOTOS_5_SHARED_PHOTO_PATH
|
||||
)
|
||||
|
||||
if self.isphoto:
|
||||
return os.path.join(
|
||||
self._db._library_path,
|
||||
shared_path,
|
||||
self._info["directory"],
|
||||
self._info["filename"],
|
||||
)
|
||||
|
||||
# a shared video has two files, the poster image and the video
|
||||
# the poster (image frame shown in Photos) is named UUID.poster.JPG
|
||||
# the video file is named UUID.medium.MP4
|
||||
# this method returns the path to the video file
|
||||
filename = f"{self.uuid}.medium.MP4"
|
||||
return os.path.join(
|
||||
self._db._library_path,
|
||||
shared_path,
|
||||
self._info["directory"],
|
||||
filename,
|
||||
)
|
||||
|
||||
def _path_4(self):
|
||||
"""return path for photo on Photos <= version 4"""
|
||||
"""Returns candidate path for original photo on Photos <= version 4"""
|
||||
if self._info["has_raw"]:
|
||||
# return the path to JPEG even if RAW is original
|
||||
vol = (
|
||||
@@ -203,9 +229,6 @@ class PhotoInfo:
|
||||
photopath = os.path.join(
|
||||
self._db._masters_path, self._info["imagePath"]
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
photopath = None
|
||||
self._path = photopath
|
||||
return photopath
|
||||
|
||||
@property
|
||||
@@ -217,14 +240,20 @@ class PhotoInfo:
|
||||
return self._path_edited
|
||||
except AttributeError:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
self._path_edited = self._path_edited_4()
|
||||
photopath = self._path_edited_4()
|
||||
else:
|
||||
self._path_edited = self._path_edited_5()
|
||||
photopath = self._path_edited_5()
|
||||
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
self._path_edited = photopath
|
||||
return self._path_edited
|
||||
|
||||
def _path_edited_5(self):
|
||||
"""return path_edited for Photos >= 5"""
|
||||
"""Returns candidate path_edited for Photos >= 5 or None if cannot be determined"""
|
||||
# In Photos 5.0 / Catalina / MacOS 10.15:
|
||||
# edited photos appear to always be converted to .jpeg and stored in
|
||||
# library_name/resources/renders/X/UUID_1_201_a.jpeg
|
||||
@@ -259,95 +288,123 @@ class PhotoInfo:
|
||||
logging.debug(f"WARNING: unknown type {self._info['type']}")
|
||||
return None
|
||||
|
||||
photopath = os.path.join(
|
||||
library, "resources", "renders", directory, filename
|
||||
)
|
||||
return os.path.join(library, "resources", "renders", directory, filename)
|
||||
|
||||
if not os.path.isfile(photopath):
|
||||
return None
|
||||
|
||||
def _get_predicted_path_edited_4(self) -> str | None:
|
||||
"""return predicted path_edited for Photos <= 4"""
|
||||
edit_id = self._info["edit_resource_id_photo"]
|
||||
folder_id, file_id, nn_id = _get_resource_loc(edit_id)
|
||||
# figure out what kind it is and build filename
|
||||
library = self._db._library_path
|
||||
if uti_edited := self.uti_edited:
|
||||
ext = get_preferred_uti_extension(uti_edited)
|
||||
if ext is not None:
|
||||
filename = f"fullsizeoutput_{file_id}.{ext}"
|
||||
return os.path.join(
|
||||
library, "resources", "media", "version", folder_id, nn_id, filename
|
||||
)
|
||||
|
||||
# if we get here, we couldn't figure out the extension
|
||||
# so try to figure out the type and build the filename
|
||||
type_ = self._info["type"]
|
||||
if type_ == _PHOTO_TYPE:
|
||||
# it's a photo
|
||||
filename = f"fullsizeoutput_{file_id}.jpeg"
|
||||
elif type_ == _MOVIE_TYPE:
|
||||
# it's a movie
|
||||
filename = f"fullsizeoutput_{file_id}.mov"
|
||||
else:
|
||||
raise ValueError(f"Unknown type {type_}")
|
||||
|
||||
return os.path.join(
|
||||
library, "resources", "media", "version", folder_id, nn_id, filename
|
||||
)
|
||||
|
||||
def _path_edited_4(self) -> str | None:
|
||||
"""return path_edited for Photos <= 4; modified version of code in PhotoInfo to debug #859"""
|
||||
|
||||
if not self._info["hasAdjustments"]:
|
||||
return None
|
||||
|
||||
if edit_id := self._info["edit_resource_id"]:
|
||||
try:
|
||||
photopath = self._get_predicted_path_edited_4()
|
||||
except ValueError as e:
|
||||
logging.debug(f"ERROR: {e}")
|
||||
photopath = None
|
||||
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
# the heuristic failed, so try to find the file
|
||||
rootdir = pathlib.Path(photopath).parent.parent
|
||||
filename = pathlib.Path(photopath).name
|
||||
for dirname, _, filelist in os.walk(rootdir):
|
||||
if filename in filelist:
|
||||
photopath = os.path.join(dirname, filename)
|
||||
break
|
||||
|
||||
# check again to see if we found a valid file
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
|
||||
# TODO: might be possible for original/master to be missing but edit to still be there
|
||||
# if self._info["isMissing"] == 1:
|
||||
# photopath = None # path would be meaningless until downloaded
|
||||
|
||||
return photopath
|
||||
|
||||
def _path_edited_4(self):
|
||||
"""return path_edited for Photos <= 4"""
|
||||
|
||||
if self._db._db_version > _PHOTOS_4_VERSION:
|
||||
raise RuntimeError("Wrong database format!")
|
||||
|
||||
photopath = None
|
||||
if self._info["hasAdjustments"]:
|
||||
edit_id = self._info["edit_resource_id"]
|
||||
if edit_id is not None:
|
||||
library = self._db._library_path
|
||||
folder_id, file_id = _get_resource_loc(edit_id)
|
||||
# todo: is this always true or do we need to search file file_id under folder_id
|
||||
# figure out what kind it is and build filename
|
||||
filename = None
|
||||
if self._info["type"] == _PHOTO_TYPE:
|
||||
# it's a photo
|
||||
filename = f"fullsizeoutput_{file_id}.jpeg"
|
||||
elif self._info["type"] == _MOVIE_TYPE:
|
||||
# it's a movie
|
||||
filename = f"fullsizeoutput_{file_id}.mov"
|
||||
else:
|
||||
# don't know what it is!
|
||||
logging.debug(f"WARNING: unknown type {self._info['type']}")
|
||||
return None
|
||||
|
||||
# photopath appears to usually be in "00" subfolder but
|
||||
# could be elsewhere--I haven't figured out this logic yet
|
||||
# first see if it's in 00
|
||||
photopath = os.path.join(
|
||||
library, "resources", "media", "version", folder_id, "00", filename
|
||||
)
|
||||
|
||||
if not os.path.isfile(photopath):
|
||||
rootdir = os.path.join(
|
||||
library, "resources", "media", "version", folder_id
|
||||
)
|
||||
|
||||
for dirname, _, filelist in os.walk(rootdir):
|
||||
if filename in filelist:
|
||||
photopath = os.path.join(dirname, filename)
|
||||
break
|
||||
|
||||
# check again to see if we found a valid file
|
||||
if not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
logging.debug(
|
||||
f"{self.uuid} hasAdjustments but edit_resource_id is None"
|
||||
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
logging.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
|
||||
photopath = None
|
||||
|
||||
return photopath
|
||||
|
||||
@property
|
||||
def path_edited_live_photo(self):
|
||||
"""return path to edited version of live photo movie; only valid for Photos 5+"""
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
return None
|
||||
|
||||
"""return path to edited version of live photo movie"""
|
||||
try:
|
||||
return self._path_edited_live_photo
|
||||
except AttributeError:
|
||||
self._path_edited_live_photo = self._path_edited_5_live_photo()
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
self._path_edited_live_photo = self._path_edited_4_live_photo()
|
||||
else:
|
||||
self._path_edited_live_photo = self._path_edited_5_live_photo()
|
||||
return self._path_edited_live_photo
|
||||
|
||||
def _get_predicted_path_edited_live_photo_4(self) -> str | None:
|
||||
"""return predicted path_edited for Photos <= 4"""
|
||||
# need the resource id for the video, not the photo (edit_resource_id is for photo)
|
||||
if edit_id := self._info["edit_resource_id_video"]:
|
||||
folder_id, file_id, nn_id = _get_resource_loc(edit_id)
|
||||
# figure out what kind it is and build filename
|
||||
library = self._db._library_path
|
||||
filename = f"videocomplementoutput_{file_id}.mov"
|
||||
return os.path.join(
|
||||
library, "resources", "media", "version", folder_id, nn_id, filename
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _path_edited_4_live_photo(self):
|
||||
"""return path_edited_live_photo for Photos <= 4"""
|
||||
if self._db._db_version > _PHOTOS_4_VERSION:
|
||||
raise RuntimeError("Wrong database format!")
|
||||
photopath = self._get_predicted_path_edited_live_photo_4()
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
# the heuristic failed, so try to find the file
|
||||
rootdir = pathlib.Path(photopath).parent.parent
|
||||
filename = pathlib.Path(photopath).name
|
||||
photopath = next(
|
||||
(
|
||||
os.path.join(dirname, filename)
|
||||
for dirname, _, filelist in os.walk(rootdir)
|
||||
if filename in filelist
|
||||
),
|
||||
None,
|
||||
)
|
||||
if photopath is None:
|
||||
logging.debug(
|
||||
f"MISSING PATH: edited live photo file for UUID {self._uuid} does not appear to exist"
|
||||
)
|
||||
return photopath
|
||||
|
||||
def _path_edited_5_live_photo(self):
|
||||
"""return path_edited_live_photo for Photos >= 5"""
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
@@ -407,15 +464,11 @@ class PhotoInfo:
|
||||
else:
|
||||
filepath = os.path.join(self._db._masters_path, self._info["directory"])
|
||||
|
||||
# raw files have same name as original but with _4.raw_ext appended
|
||||
# I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4
|
||||
# see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc
|
||||
raw_file = list_directory(filepath, startswith=f"{filestem}_4")
|
||||
if not raw_file:
|
||||
photopath = None
|
||||
else:
|
||||
if raw_file := list_directory(filepath, startswith=f"{filestem}_4"):
|
||||
photopath = pathlib.Path(filepath) / raw_file[0]
|
||||
photopath = str(photopath) if photopath.is_file() else None
|
||||
else:
|
||||
photopath = None
|
||||
else:
|
||||
# is a reference
|
||||
try:
|
||||
@@ -765,8 +818,7 @@ class PhotoInfo:
|
||||
if self._db._photos_ver < 7:
|
||||
return self._info["UTI_raw"]
|
||||
|
||||
rawpath = self.path_raw
|
||||
if rawpath:
|
||||
if rawpath := self.path_raw:
|
||||
return get_uti_for_extension(pathlib.Path(rawpath).suffix)
|
||||
else:
|
||||
return None
|
||||
@@ -863,7 +915,7 @@ class PhotoInfo:
|
||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||
photopath = None
|
||||
else:
|
||||
folder_id, file_id = _get_resource_loc(live_model_id)
|
||||
folder_id, file_id, nn_id = _get_resource_loc(live_model_id)
|
||||
library_path = self._db.library_path
|
||||
photopath = os.path.join(
|
||||
library_path,
|
||||
@@ -871,7 +923,7 @@ class PhotoInfo:
|
||||
"media",
|
||||
"master",
|
||||
folder_id,
|
||||
"00",
|
||||
nn_id,
|
||||
f"jpegvideocomplement_{file_id}.mov",
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
@@ -934,17 +986,13 @@ class PhotoInfo:
|
||||
modelid = self._info["modelID"]
|
||||
if modelid is None:
|
||||
return []
|
||||
folder_id, file_id = _get_resource_loc(modelid)
|
||||
folder_id, file_id, nn_id = _get_resource_loc(modelid)
|
||||
derivatives_root = (
|
||||
pathlib.Path(self._db._library_path)
|
||||
/ f"resources/proxies/derivatives/{folder_id}"
|
||||
)
|
||||
|
||||
# photos appears to usually be in "00" subfolder but
|
||||
# could be elsewhere--I haven't figured out this logic yet
|
||||
# first see if it's in 00
|
||||
|
||||
derivatives_path = derivatives_root / "00" / file_id
|
||||
derivatives_path = derivatives_root / nn_id / file_id
|
||||
if derivatives_path.is_dir():
|
||||
files = derivatives_path.glob("*")
|
||||
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
|
||||
@@ -966,14 +1014,17 @@ class PhotoInfo:
|
||||
"""Return paths to all derivative (preview) files for shared iCloud photos in Photos >= 5"""
|
||||
directory = self._uuid[0] # first char of uuid
|
||||
# only 1 derivative for shared photos and it's called 'UUID_4_5005_c.jpeg'
|
||||
derivative_path = (
|
||||
_PHOTOS_8_SHARED_DERIVATIVE_PATH
|
||||
if self._db._photos_ver >= 8
|
||||
else _PHOTOS_5_SHARED_DERIVATIVE_PATH
|
||||
)
|
||||
derivative_path = (
|
||||
pathlib.Path(self._db._library_path)
|
||||
/ "resources/cloudsharing/resources/derivatives/masters"
|
||||
/ derivative_path
|
||||
/ f"{directory}/{self.uuid}_4_5005_c.jpeg"
|
||||
)
|
||||
if derivative_path.exists():
|
||||
return [str(derivative_path)]
|
||||
return []
|
||||
return [str(derivative_path)] if derivative_path.exists() else []
|
||||
|
||||
@property
|
||||
def panorama(self):
|
||||
@@ -1400,6 +1451,11 @@ class PhotoInfo:
|
||||
metadata = plistlib.loads(results[0])
|
||||
return metadata
|
||||
|
||||
@cached_property
|
||||
def fingerprint(self) -> str:
|
||||
"""Returns fingerprint of original photo as a string"""
|
||||
return self._info["masterFingerprint"]
|
||||
|
||||
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
||||
"""Detects text in photo and returns lists of results as (detected text, confidence)
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ def _process_faceinfo_4(photosdb):
|
||||
face["facetype"] = row[26]
|
||||
face["quality"] = row[27]
|
||||
|
||||
# Photos 5 only
|
||||
# Photos 5+ only
|
||||
face["agetype"] = None
|
||||
face["eyemakeuptype"] = None
|
||||
face["eyestate"] = None
|
||||
|
||||
@@ -832,10 +832,10 @@ class PhotosDB:
|
||||
# for compatability with Photos 5 where album kind is ZKIND
|
||||
"kind": album[7],
|
||||
"creation_date": album[8],
|
||||
"start_date": None, # Photos 5 only
|
||||
"end_date": None, # Photos 5 only
|
||||
"customsortascending": None, # Photos 5 only
|
||||
"customsortkey": None, # Photos 5 only
|
||||
"start_date": None, # Photos 5+ only
|
||||
"end_date": None, # Photos 5+ only
|
||||
"customsortascending": None, # Photos 5+ only
|
||||
"customsortkey": None, # Photos 5+ only
|
||||
}
|
||||
|
||||
# get details about folders
|
||||
@@ -953,7 +953,8 @@ class PhotosDB:
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference,
|
||||
RKMaster.importGroupUuid
|
||||
RKMaster.importGroupUuid,
|
||||
RKMaster.fingerprint
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -985,7 +986,8 @@ class PhotosDB:
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference,
|
||||
RKMaster.importGroupUuid
|
||||
RKMaster.importGroupUuid,
|
||||
RKMaster.fingerprint
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -1036,6 +1038,7 @@ class PhotosDB:
|
||||
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
|
||||
# 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)
|
||||
# 44 RKMaster.importGroupUuid -- to get date added from RKImportGroup
|
||||
# 45 RKMaster.fingerprint -- fingerprint / hash of the file
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1233,6 +1236,9 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["import_uuid"] = row[44]
|
||||
self._dbphotos[uuid]["fok_import_session"] = None
|
||||
|
||||
# fingerprint
|
||||
self._dbphotos[uuid]["masterFingerprint"] = row[45]
|
||||
|
||||
# photos 5+ only, for shared photos
|
||||
self._dbphotos[uuid]["cloudownerhashedpersonid"] = None
|
||||
|
||||
@@ -1297,7 +1303,9 @@ class PhotosDB:
|
||||
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
||||
RKModelResource.attachedModelType, RKModelResource.resourceType
|
||||
FROM RKVersion
|
||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """
|
||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId
|
||||
ORDER BY RKModelResource.modelId
|
||||
"""
|
||||
)
|
||||
|
||||
# Order of results:
|
||||
@@ -1307,8 +1315,8 @@ class PhotosDB:
|
||||
# 3 RKModelResource.resourceTag
|
||||
# 4 RKModelResource.UTI
|
||||
# 5 RKVersion.specialType
|
||||
# 6 RKModelResource.attachedModelType
|
||||
# 7 RKModelResource.resourceType
|
||||
# 6 RKModelResource.attachedModelType (2 = edit)
|
||||
# 7 RKModelResource.resourceType (4 = photo, 8 = video)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1320,18 +1328,30 @@ class PhotosDB:
|
||||
and row[1] != "UNADJUSTED"
|
||||
and row[6] == 2
|
||||
):
|
||||
if "edit_resource_id" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
# TODO: I think there should never be more than one edit but
|
||||
# I've seen this once in my library
|
||||
# should we return all edits or just most recent one?
|
||||
# For now, return most recent edit
|
||||
self._dbphotos[uuid]["edit_resource_id"] = row[2]
|
||||
self._dbphotos[uuid]["UTI_edited"] = row[4]
|
||||
resource_type = row[7]
|
||||
# UTI_edited will be set to the appropriate UTI for the edited resource below
|
||||
# a live photo that's edited will have both a photo and video resource but the photo
|
||||
# UTI will be used for the edited live photo, see #859
|
||||
if resource_type == 4:
|
||||
# photo
|
||||
if "edit_resource_id_photo" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id_photo for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
self._dbphotos[uuid]["edit_resource_id_photo"] = row[2]
|
||||
self._dbphotos[uuid]["UTI_edited_photo"] = row[4]
|
||||
elif resource_type == 8:
|
||||
# video
|
||||
if "edit_resource_id_video" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id_video for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
self._dbphotos[uuid]["edit_resource_id_video"] = row[2]
|
||||
self._dbphotos[uuid]["UTI_edited_video"] = row[4]
|
||||
|
||||
# get details on external edits
|
||||
c.execute(
|
||||
@@ -1384,9 +1404,27 @@ class PhotosDB:
|
||||
)
|
||||
|
||||
# init any uuids that had no edits or live photos
|
||||
# also initialized UTI_edited and edit_resource_id
|
||||
for uuid in self._dbphotos:
|
||||
if "edit_resource_id" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["edit_resource_id"] = None
|
||||
if "edit_resource_id_photo" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["edit_resource_id_photo"] = None
|
||||
if "edit_resource_id_video" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["edit_resource_id_video"] = None
|
||||
if "UTI_edited_photo" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["UTI_edited_photo"] = None
|
||||
if "UTI_edited_video" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["UTI_edited_video"] = None
|
||||
# UTI_edited will be set to the appropriate UTI for the edited resource below
|
||||
# a live photo that's edited will have both a photo and video resource but the photo
|
||||
# UTI will be used for the edited live photo
|
||||
self._dbphotos[uuid]["UTI_edited"] = (
|
||||
self._dbphotos[uuid]["UTI_edited_photo"]
|
||||
or self._dbphotos[uuid]["UTI_edited_video"]
|
||||
)
|
||||
self._dbphotos[uuid]["edit_resource_id"] = (
|
||||
self._dbphotos[uuid]["edit_resource_id_photo"]
|
||||
or self._dbphotos[uuid]["edit_resource_id_video"]
|
||||
)
|
||||
if "live_model_id" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["live_model_id"] = None
|
||||
self._dbphotos[uuid]["modeResourceIsOnDisk"] = None
|
||||
@@ -1792,7 +1830,8 @@ class PhotosDB:
|
||||
"parentfolder": album[7],
|
||||
"pk": album[8],
|
||||
"intrash": False if album[9] == 0 else True,
|
||||
"creation_date": album[10] or 0, # iPhone Photos.sqlite can have null value
|
||||
"creation_date": album[10]
|
||||
or 0, # iPhone Photos.sqlite can have null value
|
||||
"start_date": album[11] or 0,
|
||||
"end_date": album[12] or 0,
|
||||
"customsortascending": album[13],
|
||||
@@ -2169,6 +2208,12 @@ class PhotosDB:
|
||||
info["alt_master_uuid"] = None # Photos 4
|
||||
info["raw_info"] = None # Photos 4
|
||||
|
||||
# Photos 4 only
|
||||
info["edit_resource_id_photo"] = None
|
||||
info["edit_resource_id_video"] = None
|
||||
info["UTI_edited_photo"] = None
|
||||
info["UTI_edited_video"] = None
|
||||
|
||||
self._dbphotos[uuid] = info
|
||||
|
||||
# compute signatures for finding possible duplicates
|
||||
@@ -3352,7 +3397,7 @@ class PhotosDB:
|
||||
if n in p.filename or n in p.original_filename
|
||||
]
|
||||
)
|
||||
photos = photo_list
|
||||
photos = list(set(photo_list))
|
||||
|
||||
if options.min_size:
|
||||
photos = [
|
||||
@@ -3459,7 +3504,7 @@ class PhotosDB:
|
||||
exifdata_value = str(exifdata_value)
|
||||
if exifvalue in exifdata_value:
|
||||
matching_photos.append(p)
|
||||
photos = matching_photos
|
||||
photos = list(set(matching_photos))
|
||||
|
||||
if options.added_after:
|
||||
added_after = options.added_after
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import logging
|
||||
import pathlib
|
||||
import plistlib
|
||||
import sys
|
||||
|
||||
from .._constants import (
|
||||
_PHOTOS_2_VERSION,
|
||||
@@ -21,7 +22,6 @@ __all__ = [
|
||||
"get_db_version",
|
||||
"get_model_version",
|
||||
"get_db_model_version",
|
||||
"UnknownLibraryVersion",
|
||||
"get_photos_library_version",
|
||||
]
|
||||
|
||||
@@ -63,7 +63,7 @@ def get_db_version(db_file):
|
||||
if version not in _TESTED_DB_VERSIONS:
|
||||
print(
|
||||
f"WARNING: Only tested on database versions [{', '.join(_TESTED_DB_VERSIONS)}]"
|
||||
+ f" You have database version={version} which has not been tested"
|
||||
+ f" You have database version={version} which has not been tested", file=sys.stderr
|
||||
)
|
||||
|
||||
return version
|
||||
@@ -115,10 +115,6 @@ def get_db_model_version(db_file: str) -> int:
|
||||
return 8
|
||||
|
||||
|
||||
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)
|
||||
@@ -140,4 +136,9 @@ def get_photos_library_version(library_path):
|
||||
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}")
|
||||
if _PHOTOS_8_MODEL_VERSION[0] <= model_ver <= _PHOTOS_8_MODEL_VERSION[1]:
|
||||
return 8
|
||||
logging.warning(
|
||||
f"Unknown db / model version: db_ver={db_ver}, model_ver={model_ver}; assuming Photos 8"
|
||||
)
|
||||
return 8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" ScoreInfo class to expose computed score info from the library """
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from ._constants import _PHOTOS_4_VERSION
|
||||
|
||||
@@ -38,3 +38,7 @@ class ScoreInfo:
|
||||
well_chosen_subject: float
|
||||
well_framed_subject: float
|
||||
well_timed_shot: float
|
||||
|
||||
def asdict(self):
|
||||
"""Return ScoreInfo as a dict"""
|
||||
return asdict(self)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||
""" get UTI for a given file extension and the preferred extension for a given UTI """
|
||||
""" get UTI for a given file extension and the preferred extension for a given UTI
|
||||
|
||||
""" Implementation note: runs only on macOS
|
||||
Implementation note: runs only on macOS
|
||||
|
||||
On macOS <= 11 (Big Sur), uses objective C CoreServices methods
|
||||
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
|
||||
@@ -17,6 +16,8 @@ __all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||
It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
@@ -27,6 +28,8 @@ import objc
|
||||
|
||||
from .utils import _get_os_version
|
||||
|
||||
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||
|
||||
# cached values of all the UTIs (< 6 chars long) known to my Mac running macOS 10.15.7
|
||||
UTI_CSV = """extension,UTI,preferred_extension,MIME_type
|
||||
c,public.c-source,c,None
|
||||
@@ -565,7 +568,7 @@ def _get_ext_from_uti_dict(uti):
|
||||
return None
|
||||
|
||||
|
||||
def get_preferred_uti_extension(uti):
|
||||
def get_preferred_uti_extension(uti: str) -> str | None:
|
||||
"""get preferred extension for a UTI type
|
||||
uti: UTI str, e.g. 'public.jpeg'
|
||||
returns: preferred extension as str or None if cannot be determined"""
|
||||
@@ -582,7 +585,7 @@ def get_preferred_uti_extension(uti):
|
||||
|
||||
# on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC
|
||||
if uti == "public.heic":
|
||||
return "HEIC"
|
||||
return "heic"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -101,22 +101,27 @@ def _check_file_exists(filename):
|
||||
return os.path.exists(filename) and not os.path.isdir(filename)
|
||||
|
||||
|
||||
def _get_resource_loc(model_id):
|
||||
"""returns folder_id and file_id needed to find location of edited photo"""
|
||||
""" and live photos for version <= Photos 4.0 """
|
||||
def _get_resource_loc(model_id) -> tuple[str, str, str]:
|
||||
"""returns folder_id and file_id needed to find location of edited photo
|
||||
and live photos for version <= Photos 4.0
|
||||
modified version of code in utils to debug #859
|
||||
"""
|
||||
# determine folder where Photos stores edited version
|
||||
# edited images are stored in:
|
||||
# Photos Library.photoslibrary/resources/media/version/XX/00/fullsizeoutput_Y.jpeg
|
||||
# Photos Library.photoslibrary/resources/media/version/folder_id/nn/fullsizeoutput_file_id.jpeg
|
||||
# where XX and Y are computed based on RKModelResources.modelId
|
||||
|
||||
# file_id (Y in above example) is hex representation of model_id without leading 0x
|
||||
file_id = hex_id = hex(model_id)[2:]
|
||||
|
||||
# folder_id (XX) in above example if first two chars of model_id converted to hex
|
||||
# folder_id (XX) is digits -4 and -3 of hex representation of model_id
|
||||
# and left padded with zeros if < 4 digits
|
||||
folder_id = hex_id.zfill(4)[0:2]
|
||||
folder_id = hex_id.zfill(4)[-4:-2]
|
||||
|
||||
return folder_id, file_id
|
||||
# find the nn_id which is the hex_id digits minus the last 4 chars (or 00 if len(hex_id) <= 4)
|
||||
nn_id = hex_id[: len(hex_id) - 4].zfill(2) if len(hex_id) > 4 else "00"
|
||||
|
||||
return folder_id, file_id, nn_id
|
||||
|
||||
|
||||
def _dd_to_dms(dd):
|
||||
|
||||
@@ -8,7 +8,7 @@ objexplore>=1.6.3,<2.0.0
|
||||
osxmetadata>=1.2.0,<2.0.0
|
||||
packaging>=21.3
|
||||
pathvalidate>=2.4.1,<2.5.0
|
||||
photoscript>=0.2.1,<0.3.0
|
||||
photoscript>=0.3.0,<0.4.0
|
||||
ptpython>=3.0.20,<3.1.0
|
||||
pyobjc-core>=9.0,<10.0
|
||||
pyobjc-framework-AVFoundation>=9.0,<10.0
|
||||
@@ -25,6 +25,7 @@ requests>=2.27.1,<3.0.0
|
||||
rich>=11.2.0,<13.0.0
|
||||
rich_theme_manager>=0.11.0
|
||||
shortuuid==1.0.9
|
||||
strpdatetime>=0.2.0
|
||||
tenacity>=8.0.1,<9.0.0
|
||||
textx>=3.0.0,<4.0.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
|
||||
3
setup.py
3
setup.py
@@ -83,7 +83,7 @@ setup(
|
||||
"osxmetadata>=1.2.0,<2.0.0",
|
||||
"packaging>=21.3",
|
||||
"pathvalidate>=2.4.1,<3.0.0",
|
||||
"photoscript>=0.2.1,<0.3.0",
|
||||
"photoscript>=0.3.0,<0.4.0",
|
||||
"ptpython>=3.0.20,<4.0.0",
|
||||
"pyobjc-core>=9.0,<=10.0",
|
||||
"pyobjc-framework-AVFoundation>=9.0,<10.0",
|
||||
@@ -100,6 +100,7 @@ setup(
|
||||
"rich>=11.2.0,<13.0.0",
|
||||
"rich_theme_manager>=0.11.0",
|
||||
"shortuuid==1.0.9",
|
||||
"strpdatetime>=0.2.0",
|
||||
"tenacity>=8.0.1,<9.0.0",
|
||||
"textx>=3.0.0,<4.0.0",
|
||||
"toml>=0.10.2,<0.11.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ A couple of tests require interaction with Photos and configuring a specific tes
|
||||
## Test Photo Libraries
|
||||
**Important**: The test code uses several test photo libraries created on various version of MacOS. If you need to inspect one of these or modify one for a test, make a copy of the library (for example, copy it to your ~/Pictures folder) then open the copy in Photos. Once done, copy the revised library back to the tests/ folder. If you do not do this, the Photos background process photoanalysisd will forever try to process the library resulting in updates to the database which will cause git to see changes to the file you didn't intend. I'm not aware of any way to disassociate photoanalysisd from the library once you've opened it in Photos.
|
||||
|
||||
Some of the "search_info" tests require data from my personal library on Catalina 10.15.7. The data is generated by running `tests/generate_search_info_test_data.py`
|
||||
Some of the "search_info" tests require data from my personal library on Catalina 10.15.7. The data is generated by running `python3 tests/generate_search_info_test_data.py >tests/search_info_test_data_10_15_7.json`
|
||||
|
||||
## Attribution ##
|
||||
These tests utilize a test Photos library. The test library is populated with photos from [flickr](https://www.flickr.com) and from my own photo library. All images used are licensed under Creative Commons 2.0 Attribution [license](https://creativecommons.org/licenses/by/2.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-Cloud-13.1.photoslibrary/database/Photos.sqlite
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/Photos.sqlite
Normal file
Binary file not shown.
BIN
tests/Test-Cloud-13.1.photoslibrary/database/Photos.sqlite-shm
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/Photos.sqlite-shm
Normal file
Binary file not shown.
BIN
tests/Test-Cloud-13.1.photoslibrary/database/Photos.sqlite-wal
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/Photos.sqlite-wal
Normal file
Binary file not shown.
@@ -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>Mac-mini.local</string>
|
||||
<key>hostuuid</key>
|
||||
<string>8E774325-0506-5746-9991-6B8189271107</string>
|
||||
<key>pid</key>
|
||||
<integer>30127</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
<integer>503</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
tests/Test-Cloud-13.1.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/metaSchema.db
Normal file
Binary file not shown.
BIN
tests/Test-Cloud-13.1.photoslibrary/database/photos.db
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/photos.db
Normal file
Binary file not shown.
Binary file not shown.
BIN
tests/Test-Cloud-13.1.photoslibrary/database/search/psi.sqlite
Normal file
BIN
tests/Test-Cloud-13.1.photoslibrary/database/search/psi.sqlite
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
<?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>Brunch</key>
|
||||
<array>
|
||||
<string>Dining</string>
|
||||
</array>
|
||||
<key>Brunches</key>
|
||||
<array>
|
||||
<string>Dining</string>
|
||||
</array>
|
||||
<key>Lunch</key>
|
||||
<array>
|
||||
<string>Dining</string>
|
||||
</array>
|
||||
<key>Lunches</key>
|
||||
<array>
|
||||
<string>Dining</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>SceneWhitelist</key>
|
||||
<array>
|
||||
<string>Amusement Park</string>
|
||||
<string>Animal</string>
|
||||
<string>Aquarium</string>
|
||||
<string>Art</string>
|
||||
<string>Car</string>
|
||||
<string>Cheese</string>
|
||||
<string>Food</string>
|
||||
<string>Forest</string>
|
||||
<string>Jewelry</string>
|
||||
<string>Music</string>
|
||||
<string>Night Sky</string>
|
||||
<string>Snow</string>
|
||||
<string>Tattoo</string>
|
||||
<string>Underwater</string>
|
||||
<string>Vehicle</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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/>
|
||||
<key>renamePerson</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>embeddingVersion</key>
|
||||
<string>1</string>
|
||||
<key>featureFlags</key>
|
||||
<string>319</string>
|
||||
<key>featuredContentAllowed</key>
|
||||
<string>1</string>
|
||||
<key>localeIdentifier</key>
|
||||
<string>en_US</string>
|
||||
<key>sceneTaxonomySHA</key>
|
||||
<string>64d078bafc0035e1ec26dfa565c2ac0479fcbab329fda1c16cd17e0fdbf2f4c0,4afa5d3c45c08a664cf73cff957aaeeae3a325d2970aada51268407b9ad0f03e</string>
|
||||
<key>searchIndexVersion</key>
|
||||
<string>16025</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user