Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
198addaa07 | ||
|
|
d91fc93737 | ||
|
|
5c3360f29d | ||
|
|
d4513832a6 | ||
|
|
f8616acf16 | ||
|
|
addd952aa3 | ||
|
|
773b619e24 | ||
|
|
683dfe7f3f | ||
|
|
7fa5fbaa5b | ||
|
|
e075868281 | ||
|
|
bf0589118b | ||
|
|
adc4b05602 | ||
|
|
a740d82e7f | ||
|
|
43af4d205a | ||
|
|
4d98fa9279 | ||
|
|
67e579be4c | ||
|
|
48d2223edd | ||
|
|
591f9bcc62 | ||
|
|
2284598a24 | ||
|
|
77371b6e5d | ||
|
|
8dbedef187 | ||
|
|
5c093c4352 | ||
|
|
e4fc3896f9 | ||
|
|
2fb3fa3058 | ||
|
|
3a4a8bdb0b | ||
|
|
2fed1ebe5e | ||
|
|
51f69585be | ||
|
|
8491830ee5 | ||
|
|
ebe2fc544d | ||
|
|
4e47de7589 | ||
|
|
5a696366fa | ||
|
|
44153f0251 | ||
|
|
a287bfb41f | ||
|
|
6d55851f75 | ||
|
|
7bdbf9da51 | ||
|
|
75516d91e9 | ||
|
|
8651fbe2b7 | ||
|
|
c2bf03a811 | ||
|
|
0904a6e44d | ||
|
|
8a935ffbc0 | ||
|
|
80f382906f | ||
|
|
66a4a285f3 | ||
|
|
bdc4b23f42 | ||
|
|
3cdfc8700d | ||
|
|
2f866256ad | ||
|
|
86018d5cc0 | ||
|
|
d657fc6ccd | ||
|
|
875f79b92d | ||
|
|
3a110bb6d3 | ||
|
|
e73327c164 | ||
|
|
3799594473 | ||
|
|
db430173b5 | ||
|
|
defe5cb61a | ||
|
|
f58f8dd804 | ||
|
|
2773ff7381 | ||
|
|
348ef54b30 | ||
|
|
9c18cee37e | ||
|
|
651ed50a07 | ||
|
|
248c95237c | ||
|
|
ddce731a5d | ||
|
|
f662495c46 | ||
|
|
08bc8a9723 | ||
|
|
1fd0fe5ea4 | ||
|
|
fd5976b75c | ||
|
|
088476c591 | ||
|
|
250697c4a2 | ||
|
|
eba796b684 | ||
|
|
965e10e20f | ||
|
|
61f649e59d | ||
|
|
165f9b08f5 | ||
|
|
039118c1aa | ||
|
|
27f779b16c | ||
|
|
eec960861e | ||
|
|
4d924d0826 | ||
|
|
55c088eea2 | ||
|
|
ee2750224a | ||
|
|
db1947dd1e | ||
|
|
248fdbcf02 | ||
|
|
71cb01572d | ||
|
|
51b1058785 | ||
|
|
87701822ae | ||
|
|
b67f11a3bb | ||
|
|
804e13efff | ||
|
|
504b81b720 | ||
|
|
538e8b588e | ||
|
|
c4980fc284 | ||
|
|
a7678df397 | ||
|
|
e6f45f5949 | ||
|
|
f8468c63fd | ||
|
|
0545d5e321 | ||
|
|
5de9d4f90c | ||
|
|
123ebb2cb7 | ||
|
|
2d09d382e9 | ||
|
|
5e676d3507 | ||
|
|
935865dc65 | ||
|
|
193f26bec8 | ||
|
|
7b6a0af314 | ||
|
|
549a9b3572 | ||
|
|
792247b51c | ||
|
|
568d1b36a6 | ||
|
|
d78097ccc0 | ||
|
|
ad9dcd9ed7 | ||
|
|
fb5fb8ebc7 | ||
|
|
7deac581b1 | ||
|
|
1173c00ce7 | ||
|
|
2bf83e4b1f | ||
|
|
aba50c5c73 | ||
|
|
8ca7719641 | ||
|
|
5dc2eeaf9a | ||
|
|
658e8ac096 | ||
|
|
b93d6822ac | ||
|
|
90b493b7a4 | ||
|
|
2480f2a325 | ||
|
|
a59bb5b02f | ||
|
|
7c8bfc811a | ||
|
|
7c7bf1be6b | ||
|
|
b1cab32ff4 | ||
|
|
05f111a287 | ||
|
|
83915c65ab | ||
|
|
22f44f7f40 | ||
|
|
02ef0f9a25 | ||
|
|
6347d94dfb | ||
|
|
a32c102d62 | ||
|
|
38842ff924 | ||
|
|
478715a363 | ||
|
|
74f1002b9a | ||
|
|
2f57abd23c | ||
|
|
f9a43b92c1 | ||
|
|
bf2a55d7f6 | ||
|
|
34bb7f2cdc | ||
|
|
3394c52768 | ||
|
|
27282af3b9 | ||
|
|
b7b06b9fdb | ||
|
|
29e424575a | ||
|
|
ea373c4197 | ||
|
|
f25a299309 | ||
|
|
5885b23d32 | ||
|
|
5dccdf7750 | ||
|
|
e9134f84df | ||
|
|
3872e7ae64 | ||
|
|
b3e86dffc8 | ||
|
|
4897fc4b05 | ||
|
|
1dbf22fdc9 | ||
|
|
fa58af8b88 | ||
|
|
9c9bcb08b3 | ||
|
|
b1cb99f83f | ||
|
|
d3605f6303 | ||
|
|
dce002cdfe | ||
|
|
7bd189e9b2 | ||
|
|
baa86c77f6 | ||
|
|
0d086bf851 | ||
|
|
ade98fc150 | ||
|
|
0d66759b1c | ||
|
|
d833c14ef4 | ||
|
|
34841f86c0 | ||
|
|
4cc40d24cf | ||
|
|
1ccf03e158 | ||
|
|
75888cd663 | ||
|
|
a08d0725b9 |
@@ -6,7 +6,8 @@
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"imageSize": 75,
|
||||
"badgeTemplate": "[](#contributors)",
|
||||
"commit": true,
|
||||
"commitConvention": "none",
|
||||
"contributors": [
|
||||
@@ -118,7 +119,64 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "synox",
|
||||
"name": "Aravindo Wingeier",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/2250964?v=4",
|
||||
"profile": "https://github.com/synox",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kradalby",
|
||||
"name": "Kristoffer Dalby",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/98431?v=4",
|
||||
"profile": "https://kradalby.no",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Rott-Apple",
|
||||
"name": "Rott-Apple",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/67875570?v=4",
|
||||
"profile": "https://github.com/Rott-Apple",
|
||||
"contributions": [
|
||||
"research"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "narensankar0529",
|
||||
"name": "narensankar0529",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/74054766?v=4",
|
||||
"profile": "https://github.com/narensankar0529",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "martinhrpi",
|
||||
"name": "Martin",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/19407684?v=4",
|
||||
"profile": "https://github.com/martinhrpi",
|
||||
"contributions": [
|
||||
"research",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davidjroos",
|
||||
"name": "davidjroos ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15630844?v=4",
|
||||
"profile": "https://github.com/davidjroos",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ jobs:
|
||||
build:
|
||||
|
||||
runs-on: macOS-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
.metrics
|
||||
.DS_store
|
||||
__pycache__
|
||||
.coverage
|
||||
.condaauto
|
||||
t.out
|
||||
.vscode/
|
||||
.tox/
|
||||
dist/
|
||||
build/
|
||||
working/
|
||||
osxphotos.egg-info/
|
||||
.mypy_cache/
|
||||
cli.spec
|
||||
*.pyc
|
||||
docsrc/_build/
|
||||
330
CHANGELOG.md
@@ -4,6 +4,336 @@ 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.40.10](https://github.com/RhetTbull/osxphotos/compare/v0.40.9...v0.40.10)
|
||||
|
||||
> 7 February 2021
|
||||
|
||||
- Fix for issue #366 [`5c3360f`](https://github.com/RhetTbull/osxphotos/commit/5c3360f29d52df2f804c70f37a2ca9a3f102d93c)
|
||||
|
||||
#### [v0.40.9](https://github.com/RhetTbull/osxphotos/compare/v0.40.8...v0.40.9)
|
||||
|
||||
> 7 February 2021
|
||||
|
||||
- Fixed unnecessary warning for long keywords, issue #365 [`f8616ac`](https://github.com/RhetTbull/osxphotos/commit/f8616acf167b5e73ab3e4b68dcfbf578230c330d)
|
||||
|
||||
#### [v0.40.8](https://github.com/RhetTbull/osxphotos/compare/v0.40.7...v0.40.8)
|
||||
|
||||
> 4 February 2021
|
||||
|
||||
- Implemented --in-album, --not-in-album, issue #364 [`addd952`](https://github.com/RhetTbull/osxphotos/commit/addd952aa315007852945a352b2c7c451ba5f21a)
|
||||
- Updated docs [`7fa5fba`](https://github.com/RhetTbull/osxphotos/commit/7fa5fbaa5b7c9aa1412eceef56e068dc044c91e0)
|
||||
- Updated docs Makefile [skip ci] [`683dfe7`](https://github.com/RhetTbull/osxphotos/commit/683dfe7f3ffd235659b58f403562ce2d51123cfb)
|
||||
|
||||
#### [v0.40.7](https://github.com/RhetTbull/osxphotos/compare/v0.40.6...v0.40.7)
|
||||
|
||||
> 3 February 2021
|
||||
|
||||
- Bump bleach from 3.1.4 to 3.3.0 [`#362`](https://github.com/RhetTbull/osxphotos/pull/362)
|
||||
- Fixed XMP template for issue #361 [`43af4d2`](https://github.com/RhetTbull/osxphotos/commit/43af4d205a7264e530bc2b2789d297be633391e1)
|
||||
- Updated sidecar test data [`591f9bc`](https://github.com/RhetTbull/osxphotos/commit/591f9bcc62720f7eddebba3b3dcff265907550dd)
|
||||
- Added tests for --only-new, #358 [`adc4b05`](https://github.com/RhetTbull/osxphotos/commit/adc4b056029794faddd464d22022a2a17298a924)
|
||||
- Updated tests for ExportDB, #358 [`48d2223`](https://github.com/RhetTbull/osxphotos/commit/48d2223edde4850830cc6a3f9776ce08f81a6636)
|
||||
- Added 11.2 to tested versions, #360 [`2284598`](https://github.com/RhetTbull/osxphotos/commit/2284598a24f63232c01dcf27b9982002123834ca)
|
||||
|
||||
#### [v0.40.6](https://github.com/RhetTbull/osxphotos/compare/v0.40.5...v0.40.6)
|
||||
|
||||
> 2 February 2021
|
||||
|
||||
- Add @davidjroos as a contributor [`8dbedef`](https://github.com/RhetTbull/osxphotos/commit/8dbedef1874882815afb4a885184249aae73bf9f)
|
||||
- Fixed documentation, #359 [`77371b6`](https://github.com/RhetTbull/osxphotos/commit/77371b6e5d8a9b8662b7b7d540378beb897f6988)
|
||||
|
||||
#### [v0.40.5](https://github.com/RhetTbull/osxphotos/compare/v0.40.3...v0.40.5)
|
||||
|
||||
> 1 February 2021
|
||||
|
||||
- Restructured docs [`3a4a8bd`](https://github.com/RhetTbull/osxphotos/commit/3a4a8bdb0bdd995c937e0a15f5d8f1685b73407f)
|
||||
- Refactored __main__, added sphinx docs [`51f6958`](https://github.com/RhetTbull/osxphotos/commit/51f69585be60d12f912ba08f138b9c1f74481dbd)
|
||||
- Implemented --only-new, #358 [`5c093c4`](https://github.com/RhetTbull/osxphotos/commit/5c093c43528193ed1704ed4ef1b8d841a95a81cf)
|
||||
|
||||
#### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3)
|
||||
|
||||
> 23 January 2021
|
||||
|
||||
- Fix for issue #348 [`5a69636`](https://github.com/RhetTbull/osxphotos/commit/5a696366fa37fc6eafebb64fa154eee7624819a7)
|
||||
- Fixed sidecar test data [`ebe2fc5`](https://github.com/RhetTbull/osxphotos/commit/ebe2fc544d3c89050924da331921dc6f6fa5d79a)
|
||||
- Version bump [`4e47de7`](https://github.com/RhetTbull/osxphotos/commit/4e47de7589f9df54ea1802275eabf7f9b5d943dd)
|
||||
|
||||
#### [v0.40.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.25...v0.40.2)
|
||||
|
||||
> 23 January 2021
|
||||
|
||||
- Fix for issue #353, #354 [`a287bfb`](https://github.com/RhetTbull/osxphotos/commit/a287bfb41f0ee1ab19db39e6f3eb7183093599a9)
|
||||
- Updated test data [`6d55851`](https://github.com/RhetTbull/osxphotos/commit/6d55851f75cc1818cbbdd0ab356dc9b5cc078b68)
|
||||
- Updated docs [`0904a6e`](https://github.com/RhetTbull/osxphotos/commit/0904a6e44db209520e3b8807229487770461c62f)
|
||||
|
||||
#### [v0.39.25](https://github.com/RhetTbull/osxphotos/compare/v0.39.24...v0.39.25)
|
||||
|
||||
> 19 January 2021
|
||||
|
||||
- face region fixes for mirrored images [`bdc4b23`](https://github.com/RhetTbull/osxphotos/commit/bdc4b23f42f5636834d1246234fa0f88089c71a4)
|
||||
|
||||
#### [v0.39.24](https://github.com/RhetTbull/osxphotos/compare/v0.39.23...v0.39.24)
|
||||
|
||||
> 19 January 2021
|
||||
|
||||
- Fixed face regions for exif orientation 6, 8 [`86018d5`](https://github.com/RhetTbull/osxphotos/commit/86018d5cc0d964760fd64047ce52f1f54fc28dc0)
|
||||
- version bump [`2f86625`](https://github.com/RhetTbull/osxphotos/commit/2f866256adfdf39244241ca6bbcc7a8d072555b9)
|
||||
|
||||
#### [v0.39.23](https://github.com/RhetTbull/osxphotos/compare/v0.39.22...v0.39.23)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Fixed face region orientation [`875f79b`](https://github.com/RhetTbull/osxphotos/commit/875f79b92d9510e59fe8ca0aa21a42abc7600f70)
|
||||
- Updated documentation for new face region properties [`3a110bb`](https://github.com/RhetTbull/osxphotos/commit/3a110bb6d3d23d1c9fd8612b4201144046fed567)
|
||||
|
||||
#### [v0.39.22](https://github.com/RhetTbull/osxphotos/compare/v0.39.21...v0.39.22)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Beta fix for Digikam reading XMP [`3799594`](https://github.com/RhetTbull/osxphotos/commit/379959447373f951ffca372598ea8f1d5834fe52)
|
||||
- Add @martinhrpi as a contributor [`db43017`](https://github.com/RhetTbull/osxphotos/commit/db430173b59732f944ca52b53c928370684580df)
|
||||
|
||||
#### [v0.39.21](https://github.com/RhetTbull/osxphotos/compare/v0.39.20...v0.39.21)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Added beta support for face regions in xmp [`2773ff7`](https://github.com/RhetTbull/osxphotos/commit/2773ff73815ef4667f88a45b016539e490d31769)
|
||||
- Fixed osxphotos.spec datas [`f58f8dd`](https://github.com/RhetTbull/osxphotos/commit/f58f8dd804f432d07048b98e5dcedca57fec0a5e)
|
||||
|
||||
#### [v0.39.20](https://github.com/RhetTbull/osxphotos/compare/v0.39.19...v0.39.20)
|
||||
|
||||
> 16 January 2021
|
||||
|
||||
- Added isreference property and --is-reference, #321 [`651ed50`](https://github.com/RhetTbull/osxphotos/commit/651ed50a076bd3685c7d7a568e53960363d5c30b)
|
||||
- version bump [`9c18cee`](https://github.com/RhetTbull/osxphotos/commit/9c18cee37e961d2e1059490ad1dbe4e45c501002)
|
||||
|
||||
#### [v0.39.19](https://github.com/RhetTbull/osxphotos/compare/v0.39.18...v0.39.19)
|
||||
|
||||
> 15 January 2021
|
||||
|
||||
- Added retry to use_photos_export, issue #351 [`ddce731`](https://github.com/RhetTbull/osxphotos/commit/ddce731a5d354e833d56a64d06cdbc39711f693e)
|
||||
|
||||
#### [v0.39.18](https://github.com/RhetTbull/osxphotos/compare/v0.39.17...v0.39.18)
|
||||
|
||||
> 15 January 2021
|
||||
|
||||
- Fixed XMP sidecars to conform with exiftool format, #349, #350 [`1fd0fe5`](https://github.com/RhetTbull/osxphotos/commit/1fd0fe5ea477ccea43c78086af440bd32dc702d8)
|
||||
- Added update_readme.py to auto-build README [`fd5976b`](https://github.com/RhetTbull/osxphotos/commit/fd5976b75c79a3d205db2e8132c388de95632b77)
|
||||
- Added modified.strftime template, refactored test_template.py [`088476c`](https://github.com/RhetTbull/osxphotos/commit/088476c59126c6d6fe75551ff122e81aababf818)
|
||||
|
||||
#### [v0.39.17](https://github.com/RhetTbull/osxphotos/compare/v0.39.16...v0.39.17)
|
||||
|
||||
> 12 January 2021
|
||||
|
||||
- Fixed test for M1, added about command, closes #315 [`#315`](https://github.com/RhetTbull/osxphotos/issues/315)
|
||||
- Fixed time zone for tests [`165f9b0`](https://github.com/RhetTbull/osxphotos/commit/165f9b08f5056d1f0b2ca7c74cec84d42b635663)
|
||||
- Add @narensankar0529 as a contributor [`039118c`](https://github.com/RhetTbull/osxphotos/commit/039118c1aaa217f46354b351ea36b0729e3e1c35)
|
||||
- Update @narensankar0529 as a contributor [`61f649e`](https://github.com/RhetTbull/osxphotos/commit/61f649e59d53a3e3011602476b72cc64951d38c0)
|
||||
|
||||
#### [v0.39.16](https://github.com/RhetTbull/osxphotos/compare/v0.39.15...v0.39.16)
|
||||
|
||||
> 12 January 2021
|
||||
|
||||
- Added version check for M1 macs [`27f779b`](https://github.com/RhetTbull/osxphotos/commit/27f779b16c850cdbda2691e5fae8cd14405653b3)
|
||||
|
||||
#### [v0.39.15](https://github.com/RhetTbull/osxphotos/compare/v0.39.13...v0.39.15)
|
||||
|
||||
> 11 January 2021
|
||||
|
||||
- Completed implementation of --jpeg-ext, fixed --dry-run, closes #330, #346 [`#330`](https://github.com/RhetTbull/osxphotos/issues/330)
|
||||
- Added --jpeg-ext, implements #330 [`55c088e`](https://github.com/RhetTbull/osxphotos/commit/55c088eea2ddecb14e362221da9e2a7c0f403780)
|
||||
|
||||
#### [v0.39.13](https://github.com/RhetTbull/osxphotos/compare/v0.39.12...v0.39.13)
|
||||
|
||||
> 10 January 2021
|
||||
|
||||
- Fixed leaky memory in PhotoKit, issue #276 [`db1947d`](https://github.com/RhetTbull/osxphotos/commit/db1947dd1e3d47a487eeb68a5ceb5f7098f1df10)
|
||||
|
||||
#### [v0.39.12](https://github.com/RhetTbull/osxphotos/compare/v0.39.11...v0.39.12)
|
||||
|
||||
> 9 January 2021
|
||||
|
||||
- Force cleanup of objects in write_jpeg (fix memory leak) [`#344`](https://github.com/RhetTbull/osxphotos/pull/344)
|
||||
- doc: Recorded screencast and updated of readme [skip ci] [`#328`](https://github.com/RhetTbull/osxphotos/pull/328)
|
||||
- Added PhotoInfo.visible, PhotoInfo.date_trashed, closes #333, #334 [`#333`](https://github.com/RhetTbull/osxphotos/issues/333)
|
||||
- Force cleanup of objects with autorelease pool [`b67f11a`](https://github.com/RhetTbull/osxphotos/commit/b67f11a3bb95c08a39a185b6d884092870e949f2)
|
||||
- Add @Rott-Apple as a contributor [`71cb015`](https://github.com/RhetTbull/osxphotos/commit/71cb01572d2d946df18dd7b36f95b2f2e5b48f86)
|
||||
- Updated README [skip ci] [`804e13e`](https://github.com/RhetTbull/osxphotos/commit/804e13efff921ab51b996493d659b32102807a8a)
|
||||
|
||||
#### [v0.39.11](https://github.com/RhetTbull/osxphotos/compare/v0.39.10...v0.39.11)
|
||||
|
||||
> 8 January 2021
|
||||
|
||||
- All contributors/add kradalby [`#343`](https://github.com/RhetTbull/osxphotos/pull/343)
|
||||
- Ensure keyword list only contains strings, @all-contributors please add @kradalby for code [`#342`](https://github.com/RhetTbull/osxphotos/pull/342)
|
||||
- Added README.rst, closes #331 [`#331`](https://github.com/RhetTbull/osxphotos/issues/331)
|
||||
- Updated tests workflow badge link [`a7678df`](https://github.com/RhetTbull/osxphotos/commit/a7678df3974ff539050f5acb4c94817f525dcd56)
|
||||
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`e6f45f5`](https://github.com/RhetTbull/osxphotos/commit/e6f45f59491d9e805e227af8cbf8ac08ff99fdf0)
|
||||
- Renamed workflow to tests [`f8468c6`](https://github.com/RhetTbull/osxphotos/commit/f8468c63fda930216f73ad5aa8c4aa92edf1adf2)
|
||||
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`5de9d4f`](https://github.com/RhetTbull/osxphotos/commit/5de9d4f90c1102c4fb0099befd6142180f32df3f)
|
||||
- Ensure merge_exif_keywords are str not int [`123ebb2`](https://github.com/RhetTbull/osxphotos/commit/123ebb2cb752bb94291ac2b77e4a327cee996df1)
|
||||
|
||||
#### [v0.39.10](https://github.com/RhetTbull/osxphotos/compare/v0.39.9...v0.39.10)
|
||||
|
||||
> 6 January 2021
|
||||
|
||||
- Refactored ExportResults [`568d1b3`](https://github.com/RhetTbull/osxphotos/commit/568d1b36a631df33317dc00f27126b507c90bf51)
|
||||
- Improved handling of deleted photos, #332 [`792247b`](https://github.com/RhetTbull/osxphotos/commit/792247b51cc2263221ba8c2e741d2ec454c75ca8)
|
||||
- Added error_str to ExportResults [`d78097c`](https://github.com/RhetTbull/osxphotos/commit/d78097ccc0686680baf5fffa91f9e082e44b576e)
|
||||
|
||||
#### [v0.39.9](https://github.com/RhetTbull/osxphotos/compare/v0.39.8...v0.39.9)
|
||||
|
||||
> 4 January 2021
|
||||
|
||||
- Added test for Big Sur 16.0.1 database changes [`7deac58`](https://github.com/RhetTbull/osxphotos/commit/7deac581b1f1fb3dc59885b6e1ab9a63b382408d)
|
||||
- Create terminalizer-demo.yml [`5dc2eea`](https://github.com/RhetTbull/osxphotos/commit/5dc2eeaf9a7265873c81db23bbc86d3023189a26)
|
||||
- doc: Recorded screencast and updated of readme [`658e8ac`](https://github.com/RhetTbull/osxphotos/commit/658e8ac096d141fce48483dbfc1426bea317d806)
|
||||
|
||||
#### [v0.39.8](https://github.com/RhetTbull/osxphotos/compare/v0.39.7...v0.39.8)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- Updated README, version [`b93d682`](https://github.com/RhetTbull/osxphotos/commit/b93d6822ac5366c57d9142cba9b809b4ab99ad98)
|
||||
|
||||
#### [v0.39.7](https://github.com/RhetTbull/osxphotos/compare/v0.39.6...v0.39.7)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- doc: start with examples before the export reference, thanks to @synox [`#327`](https://github.com/RhetTbull/osxphotos/pull/327)
|
||||
- Added tag_groups arg to ExifTool.asdict(), issue #324 [`2480f2a`](https://github.com/RhetTbull/osxphotos/commit/2480f2a325dbb09689f8c417618b7b9e976bfcb9)
|
||||
- doc: start with examples before the export reference [`7c7bf1b`](https://github.com/RhetTbull/osxphotos/commit/7c7bf1be6b6382a995a4e17906adfd8720d0a1c3)
|
||||
- Updated dependencies in README.md [`b1cab32`](https://github.com/RhetTbull/osxphotos/commit/b1cab32ff4c7b65ae4c9a5a9a11c175dbd487c0a)
|
||||
- remove extra spaces [`a59bb5b`](https://github.com/RhetTbull/osxphotos/commit/a59bb5b02f10fa554dae346a7271be37f50d8bcc)
|
||||
- Adding back dependency https://github.com/RhetTbull/PhotoScript) [`7c8bfc8`](https://github.com/RhetTbull/osxphotos/commit/7c8bfc811ab3a93dabadf1655f7d0e217d6c7b01)
|
||||
|
||||
#### [v0.39.6](https://github.com/RhetTbull/osxphotos/compare/v0.39.5...v0.39.6)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- Make readme easier for beginners, thanks to @synox [`#326`](https://github.com/RhetTbull/osxphotos/pull/326)
|
||||
- doc simplify readme [`02ef0f9`](https://github.com/RhetTbull/osxphotos/commit/02ef0f9a254e83a3729a09cea1ae523407074896)
|
||||
- Added exception handling/capture for convert-to-jpeg, issue #322 [`05f111a`](https://github.com/RhetTbull/osxphotos/commit/05f111a287e882ed6b451a550a87753501316aba)
|
||||
- Add @synox as a contributor [`83915c6`](https://github.com/RhetTbull/osxphotos/commit/83915c65abb880036f80ebd830eb1e34292f9599)
|
||||
|
||||
#### [v0.39.5](https://github.com/RhetTbull/osxphotos/compare/v0.39.4...v0.39.5)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- Cleanup up the readme [`38842ff`](https://github.com/RhetTbull/osxphotos/commit/38842ff9249e6f5b3069a88a759c8df97ddce51c)
|
||||
|
||||
#### [v0.39.4](https://github.com/RhetTbull/osxphotos/compare/v0.39.3...v0.39.4)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- Implemented text replacement for templates, issue #316 [`478715a`](https://github.com/RhetTbull/osxphotos/commit/478715a363f5009e4a38148e832bf0ad3c4cc4f8)
|
||||
|
||||
#### [v0.39.3](https://github.com/RhetTbull/osxphotos/compare/v0.39.2...v0.39.3)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Fixed modified template to use creation time if no modificationd date, issue #312 [`2f57abd`](https://github.com/RhetTbull/osxphotos/commit/2f57abd23cabe57bcf667a1713c37689b330a702)
|
||||
|
||||
#### [v0.39.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.1...v0.39.2)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Added --xattr-template, closes #242 [`#242`](https://github.com/RhetTbull/osxphotos/issues/242)
|
||||
|
||||
#### [v0.39.1](https://github.com/RhetTbull/osxphotos/compare/v0.39.0...v0.39.1)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Fixed --exiftool-path bug, issue #311, #313 [`3394c52`](https://github.com/RhetTbull/osxphotos/commit/3394c527682d8fdd2f20f4f778d802dab86b6372)
|
||||
|
||||
#### [v0.39.0](https://github.com/RhetTbull/osxphotos/compare/v0.38.22...v0.39.0)
|
||||
|
||||
> 30 December 2020
|
||||
|
||||
- Added Finder tags, partial implementation for issue #242 [`#310`](https://github.com/RhetTbull/osxphotos/pull/310)
|
||||
- Added tests for Finder tags [`29e4245`](https://github.com/RhetTbull/osxphotos/commit/29e424575a522ae03efe5a140be46bfd0a1346c5)
|
||||
- Initial implementation for Finder tags [`5885b23`](https://github.com/RhetTbull/osxphotos/commit/5885b23d3249cf91953092a6b1ce967da2667e29)
|
||||
- Updated README for finder tags [`f25a299`](https://github.com/RhetTbull/osxphotos/commit/f25a2993097ad7b2b8ab2d1c787db58c0d799a41)
|
||||
- Updated requirements.txt [`ea373c4`](https://github.com/RhetTbull/osxphotos/commit/ea373c4197ce1cce00e89157fe560d1366f7e764)
|
||||
|
||||
#### [v0.38.22](https://github.com/RhetTbull/osxphotos/compare/v0.38.21...v0.38.22)
|
||||
|
||||
> 30 December 2020
|
||||
|
||||
- Fixed --exiftool-path bug, issue #308 [`5dccdf7`](https://github.com/RhetTbull/osxphotos/commit/5dccdf7750611c78de5356bb02f6023d4fc382c5)
|
||||
|
||||
#### [v0.38.21](https://github.com/RhetTbull/osxphotos/compare/v0.38.20...v0.38.21)
|
||||
|
||||
> 29 December 2020
|
||||
|
||||
- Fixed --exiftool-path to work with --exiftool-merge-keywords/persons [`3872e7a`](https://github.com/RhetTbull/osxphotos/commit/3872e7ae649f42d849de472a7dbf78a241d54407)
|
||||
|
||||
#### [v0.38.20](https://github.com/RhetTbull/osxphotos/compare/v0.38.19...v0.38.20)
|
||||
|
||||
> 29 December 2020
|
||||
|
||||
- Added --exiftool-path to CLI [`4897fc4`](https://github.com/RhetTbull/osxphotos/commit/4897fc4b05cc7a3bea314f9cce8a2163bf3922b2)
|
||||
|
||||
#### [v0.38.19](https://github.com/RhetTbull/osxphotos/compare/v0.38.18...v0.38.19)
|
||||
|
||||
> 29 December 2020
|
||||
|
||||
- Added exiftool signature to JSON output, issue #303 [`fa58af8`](https://github.com/RhetTbull/osxphotos/commit/fa58af8b883da11fdfa723d2da75a600d927d46e)
|
||||
|
||||
#### [v0.38.18](https://github.com/RhetTbull/osxphotos/compare/v0.38.17...v0.38.18)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added --exiftool-merge-keywords/persons, issue #299, #292 [`b1cb99f`](https://github.com/RhetTbull/osxphotos/commit/b1cb99f83f55128a314d265d4588134cb79026c6)
|
||||
|
||||
#### [v0.38.17](https://github.com/RhetTbull/osxphotos/compare/v0.38.16...v0.38.17)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added --sidecar-drop-ext, issue #291 [`dce002c`](https://github.com/RhetTbull/osxphotos/commit/dce002cdfe12fa5fa4ada4d5097828a5375c2ecd)
|
||||
- Updated Template Substitution table [`7bd189e`](https://github.com/RhetTbull/osxphotos/commit/7bd189e9b22a2ad5a8a80deb7cb93c61be37c771)
|
||||
|
||||
#### [v0.38.16](https://github.com/RhetTbull/osxphotos/compare/v0.38.15...v0.38.16)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added searchinfo templates, issue #302 [`0d086bf`](https://github.com/RhetTbull/osxphotos/commit/0d086bf85102ce78b3111c64bfa88673fbc19559)
|
||||
|
||||
#### [v0.38.15](https://github.com/RhetTbull/osxphotos/compare/v0.38.14...v0.38.15)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added --sidecar exiftool, issue #303 [`d833c14`](https://github.com/RhetTbull/osxphotos/commit/d833c14ef4b3f9375a85034cf0fb0f85a68cabb4)
|
||||
- Refactored sidecar code [`ade98fc`](https://github.com/RhetTbull/osxphotos/commit/ade98fc15051684bfb54d0199d9c370481b70dcc)
|
||||
- Refactored export2 to use sidecar bit field [`0d66759`](https://github.com/RhetTbull/osxphotos/commit/0d66759b1c200f1ecda202e28c259f88fd3db599)
|
||||
|
||||
#### [v0.38.14](https://github.com/RhetTbull/osxphotos/compare/v0.38.13...v0.38.14)
|
||||
|
||||
> 27 December 2020
|
||||
|
||||
- Bug fix for --description-template, issue #304 [`4cc40d2`](https://github.com/RhetTbull/osxphotos/commit/4cc40d24cfb11ef8668c5d3c3bab40371fdd0436)
|
||||
|
||||
#### [v0.38.13](https://github.com/RhetTbull/osxphotos/compare/v0.38.12...v0.38.13)
|
||||
|
||||
> 27 December 2020
|
||||
|
||||
- Set XMP:Subject to match Keywords, issue #302 [`75888cd`](https://github.com/RhetTbull/osxphotos/commit/75888cd6633d3f0180d24fef4f6776986a136f0f)
|
||||
|
||||
#### [v0.38.12](https://github.com/RhetTbull/osxphotos/compare/v0.38.11...v0.38.12)
|
||||
|
||||
> 26 December 2020
|
||||
|
||||
- Fixed city/sub-locality for SearchInfo [`f9f699b`](https://github.com/RhetTbull/osxphotos/commit/f9f699ba3500d58494f955d4e5d8118e336e6a2c)
|
||||
|
||||
#### [v0.38.11](https://github.com/RhetTbull/osxphotos/compare/v0.38.9...v0.38.11)
|
||||
|
||||
> 26 December 2020
|
||||
|
||||
- Exposed SearchInfo, closes #121 [`#121`](https://github.com/RhetTbull/osxphotos/issues/121)
|
||||
- Added version to --verbose, closes #297 [`#297`](https://github.com/RhetTbull/osxphotos/issues/297)
|
||||
- Added --exportdb [`2a49255`](https://github.com/RhetTbull/osxphotos/commit/2a49255277d3c6bd3b0d5f8288afd7de7dab0320)
|
||||
- Updated README.md [`f469ccc`](https://github.com/RhetTbull/osxphotos/commit/f469cccc4b4561db7611c3e9abf5aefc3ab0f648)
|
||||
- Fixed help text [`f3b7134`](https://github.com/RhetTbull/osxphotos/commit/f3b7134af1e3d07fb956eaccccd9d60bd075d3bf)
|
||||
|
||||
#### [v0.38.9](https://github.com/RhetTbull/osxphotos/compare/v0.38.8...v0.38.9)
|
||||
|
||||
> 21 December 2020
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Rhet Turnbull
|
||||
Copyright (c) 2019-2021 Rhet Turnbull
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
271
README.rst
Normal file
@@ -0,0 +1,271 @@
|
||||
.. role:: raw-html-m2r(raw)
|
||||
:format: html
|
||||
|
||||
|
||||
OSXPhotos
|
||||
=========
|
||||
|
||||
What is osxphotos?
|
||||
------------------
|
||||
|
||||
OSXPhotos provides both the ability to interact with and query Apple's Photos.app library on macOS directly from your python code
|
||||
as well as a very flexible command line interface (CLI) app for exporting photos.
|
||||
You can query the Photos library database -- for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
|
||||
You can also easily export both the original and edited photos.
|
||||
|
||||
Supported operating systems
|
||||
---------------------------
|
||||
|
||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
|
||||
Beta support for macOS Big Sur (10.16.01/11.01).
|
||||
|
||||
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.7``.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.
|
||||
|
||||
Installation using pipx
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you aren't familiar with installing python applications, I recommend you install ``osxphotos`` with `pipx <https://github.com/pipxproject/pipx>`_. If you use ``pipx``\ , you will not need to create a virtual environment as ``pipx`` takes care of this. The easiest way to do this on a Mac is to use `homebrew <https://brew.sh/>`_\ :
|
||||
|
||||
|
||||
* 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``
|
||||
* Then type this: ``pipx install osxphotos``
|
||||
* Now you should be able to run ``osxphotos`` by typing: ``osxphotos``
|
||||
|
||||
Installation using pip
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also install directly from `pypi <https://pypi.org/project/osxphotos/>`_\ :
|
||||
|
||||
.. code-block::
|
||||
|
||||
pip install osxphotos
|
||||
|
||||
|
||||
Installation from git repository
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
OSXPhotos uses setuptools, thus simply run:
|
||||
|
||||
.. code-block::
|
||||
|
||||
git clone https://github.com/RhetTbull/osxphotos.git
|
||||
cd osxphotos
|
||||
python3 setup.py install
|
||||
|
||||
|
||||
I recommend you create a `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ before installing osxphotos.
|
||||
|
||||
**WARNING** The git repo for this project is very large (> 1GB) because it contains multiple Photos libraries used for testing
|
||||
on different versions of macOS. If you just want to use the osxphotos package in your own code,
|
||||
I recommend you install the latest version from `PyPI <https://pypi.org/project/osxphotos/>`_ which does not include all the test
|
||||
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
|
||||
`release <https://github.com/RhetTbull/osxphotos/releases>`_ or you can install via ``pip`` which also installs the command line app.
|
||||
If you aren't comfortable with running python on your Mac, start with the pre-built executable or ``pipx`` as described above.
|
||||
|
||||
Command Line Usage
|
||||
------------------
|
||||
|
||||
This package will install a command line utility called ``osxphotos`` that allows you to query the Photos database and export photos.
|
||||
Alternatively, you can also run the command line utility like this: ``python3 -m osxphotos``
|
||||
|
||||
.. code-block::
|
||||
|
||||
> osxphotos
|
||||
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--db <Photos database path> Specify Photos database path. Path to Photos
|
||||
library/database can be specified using either
|
||||
--db or directly as PHOTOS_LIBRARY positional
|
||||
argument. If neither --db or PHOTOS_LIBRARY
|
||||
provided, will attempt to find the library to
|
||||
use in the following order: 1. last opened
|
||||
library, 2. system library, 3.
|
||||
~/Pictures/Photos Library.photoslibrary
|
||||
--json Print output in JSON format.
|
||||
-v, --version Show the version and exit.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
dump Print list of all photos & associated info from the Photos...
|
||||
export Export photos from the Photos database.
|
||||
help Print help; for help on commands: help <command>.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options; if...
|
||||
|
||||
To get help on a specific command, use ``osxphotos help <command_name>``
|
||||
|
||||
Command line examples
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
export all photos to ~/Desktop/export group in folders by date created
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos export --export-by-date ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export``
|
||||
|
||||
**Note**\ : Photos library/database path can also be specified using ``--db`` option:
|
||||
|
||||
``osxphotos export --export-by-date --db ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export``
|
||||
|
||||
find all photos with keyword "Kids" and output results to json file named results.json:
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos query --keyword Kids --json ~/Pictures/Photos\ Library.photoslibrary >results.json``
|
||||
|
||||
export photos to file structure based on 4-digit year and full name of month of photo's creation date:
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos export ~/Desktop/export --directory "{created.year}/{created.month}"``
|
||||
|
||||
(by default, it will attempt to use the system library)
|
||||
|
||||
export photos to file structure based on 4-digit year of photo's creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos export ~/Desktop/export --directory "{created.year}" --keyword-template "{label}" --keyword-template "{media_type}"``
|
||||
|
||||
export default library using 'country name/year' as output directory (but use "NoCountry/year" if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose``
|
||||
|
||||
Example uses of the package
|
||||
---------------------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
""" Simple usage of the package """
|
||||
import osxphotos
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.album_names)
|
||||
|
||||
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()
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
""" Export all photos to specified directory using album names as folders
|
||||
If file has been edited, also export the edited version,
|
||||
otherwise, export the original version
|
||||
This will result in duplicate photos if photo is in more than album """
|
||||
|
||||
import os.path
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import click
|
||||
from pathvalidate import is_valid_filepath, sanitize_filepath
|
||||
|
||||
import osxphotos
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("export_path", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--default-album",
|
||||
help="Default folder for photos with no album. Defaults to 'unfiled'",
|
||||
default="unfiled",
|
||||
)
|
||||
@click.option(
|
||||
"--library-path",
|
||||
help="Path to Photos library, default to last used library",
|
||||
default=None,
|
||||
)
|
||||
def export(export_path, default_album, library_path):
|
||||
export_path = os.path.expanduser(export_path)
|
||||
library_path = os.path.expanduser(library_path) if library_path else None
|
||||
|
||||
if library_path is not None:
|
||||
photosdb = osxphotos.PhotosDB(library_path)
|
||||
else:
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
|
||||
photos = photosdb.photos()
|
||||
|
||||
for p in photos:
|
||||
if not p.ismissing:
|
||||
albums = p.albums
|
||||
if not albums:
|
||||
albums = [default_album]
|
||||
for album in albums:
|
||||
click.echo(f"exporting {p.filename} in album {album}")
|
||||
|
||||
# make sure no invalid characters in destination path (could be in album name)
|
||||
album_name = sanitize_filepath(album, platform="auto")
|
||||
|
||||
# create destination folder, if necessary, based on album name
|
||||
dest_dir = os.path.join(export_path, album_name)
|
||||
|
||||
# verify path is a valid path
|
||||
if not is_valid_filepath(dest_dir, platform="auto"):
|
||||
sys.exit(f"Invalid filepath {dest_dir}")
|
||||
|
||||
# create destination dir if needed
|
||||
if not os.path.isdir(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
# export the photo
|
||||
if p.hasadjustments:
|
||||
# export edited version
|
||||
exported = p.export(dest_dir, edited=True)
|
||||
edited_name = pathlib.Path(p.path_edited).name
|
||||
click.echo(f"Exported {edited_name} to {exported}")
|
||||
# export unedited version
|
||||
exported = p.export(dest_dir)
|
||||
click.echo(f"Exported {p.filename} to {exported}")
|
||||
else:
|
||||
click.echo(f"Skipping missing photo: {p.filename}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export() # pylint: disable=no-value-for-parameter
|
||||
|
||||
Package Interface
|
||||
-----------------
|
||||
|
||||
Reference full documentation on `GitHub <https://github.com/RhetTbull/osxphotos/blob/master/README.md>`_
|
||||
5
cli.py
@@ -3,8 +3,7 @@
|
||||
To build this into an executable:
|
||||
- install pyinstaller:
|
||||
python3 -m pip install pyinstaller
|
||||
- then use make_cli_exe.sh to run pyinstaller or execute the following command:
|
||||
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
|
||||
- then use make_cli_exe.sh to run pyinstaller
|
||||
|
||||
Resulting executable will be in "dist/osxphotos"
|
||||
|
||||
@@ -13,7 +12,7 @@
|
||||
|
||||
"""
|
||||
|
||||
from osxphotos.__main__ import cli
|
||||
from osxphotos.cli import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
4
docs/.buildinfo
Normal file
@@ -0,0 +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: 17bac89a05164f8fa737ad20fcd95f80
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
105
docs/_modules/index.html
Normal file
@@ -0,0 +1,105 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overview: module code — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../" 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" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>All modules for which code is available</h1>
|
||||
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
|
||||
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
|
||||
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</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="../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>
|
||||
</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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
195
docs/_modules/osxphotos/photoinfo/_photoinfo_exifinfo.html
Normal file
@@ -0,0 +1,195 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_exifinfo — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" 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" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.photoinfo._photoinfo_exifinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" PhotoInfo methods to expose EXIF info from the library """</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">logging</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">.._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
|
||||
|
||||
|
||||
<span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">class</span> <span class="nc">ExifInfo</span><span class="p">:</span>
|
||||
<span class="sd">""" EXIF info associated with a photo from the Photos library """</span>
|
||||
|
||||
<span class="n">flash_fired</span><span class="p">:</span> <span class="nb">bool</span>
|
||||
<span class="n">iso</span><span class="p">:</span> <span class="nb">int</span>
|
||||
<span class="n">metering_mode</span><span class="p">:</span> <span class="nb">int</span>
|
||||
<span class="n">sample_rate</span><span class="p">:</span> <span class="nb">int</span>
|
||||
<span class="n">track_format</span><span class="p">:</span> <span class="nb">int</span>
|
||||
<span class="n">white_balance</span><span class="p">:</span> <span class="nb">int</span>
|
||||
<span class="n">aperture</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">bit_rate</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">duration</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">exposure_bias</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">focal_length</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">fps</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">latitude</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">longitude</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">shutter_speed</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">camera_make</span><span class="p">:</span> <span class="nb">str</span>
|
||||
<span class="n">camera_model</span><span class="p">:</span> <span class="nb">str</span>
|
||||
<span class="n">codec</span><span class="p">:</span> <span class="nb">str</span>
|
||||
<span class="n">lens_model</span><span class="p">:</span> <span class="nb">str</span>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">exif_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" Returns an ExifInfo object with the EXIF data for photo</span>
|
||||
<span class="sd"> Note: the returned EXIF data is the data Photos stores in the database on import;</span>
|
||||
<span class="sd"> ExifInfo does not provide access to the EXIF info in the actual image file</span>
|
||||
<span class="sd"> Some or all of the fields may be None</span>
|
||||
<span class="sd"> Only valid for Photos 5; on earlier database returns None</span>
|
||||
<span class="sd"> """</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"exif_info not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">exif</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">_db_exifinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
|
||||
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">ExifInfo</span><span class="p">(</span>
|
||||
<span class="n">iso</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZISO"</span><span class="p">],</span>
|
||||
<span class="n">flash_fired</span><span class="o">=</span><span class="kc">True</span> <span class="k">if</span> <span class="n">exif</span><span class="p">[</span><span class="s2">"ZFLASHFIRED"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">else</span> <span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">metering_mode</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZMETERINGMODE"</span><span class="p">],</span>
|
||||
<span class="n">sample_rate</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZSAMPLERATE"</span><span class="p">],</span>
|
||||
<span class="n">track_format</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZTRACKFORMAT"</span><span class="p">],</span>
|
||||
<span class="n">white_balance</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZWHITEBALANCE"</span><span class="p">],</span>
|
||||
<span class="n">aperture</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZAPERTURE"</span><span class="p">],</span>
|
||||
<span class="n">bit_rate</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZBITRATE"</span><span class="p">],</span>
|
||||
<span class="n">duration</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZDURATION"</span><span class="p">],</span>
|
||||
<span class="n">exposure_bias</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZEXPOSUREBIAS"</span><span class="p">],</span>
|
||||
<span class="n">focal_length</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZFOCALLENGTH"</span><span class="p">],</span>
|
||||
<span class="n">fps</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZFPS"</span><span class="p">],</span>
|
||||
<span class="n">latitude</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZLATITUDE"</span><span class="p">],</span>
|
||||
<span class="n">longitude</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZLONGITUDE"</span><span class="p">],</span>
|
||||
<span class="n">shutter_speed</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZSHUTTERSPEED"</span><span class="p">],</span>
|
||||
<span class="n">camera_make</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZCAMERAMAKE"</span><span class="p">],</span>
|
||||
<span class="n">camera_model</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZCAMERAMODEL"</span><span class="p">],</span>
|
||||
<span class="n">codec</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZCODEC"</span><span class="p">],</span>
|
||||
<span class="n">lens_model</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZLENSMODEL"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</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">"Could not find exif record 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">"</span><span class="p">)</span>
|
||||
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">ExifInfo</span><span class="p">(</span>
|
||||
<span class="n">iso</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">flash_fired</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">metering_mode</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">sample_rate</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">track_format</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">white_balance</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">aperture</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">bit_rate</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">duration</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">exposure_bias</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">focal_length</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">fps</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">latitude</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">longitude</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">shutter_speed</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">camera_make</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">camera_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">codec</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">lens_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">exif_info</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</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="../../../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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1935
docs/_modules/osxphotos/photoinfo/_photoinfo_export.html
Normal file
220
docs/_modules/osxphotos/photoinfo/_photoinfo_scoreinfo.html
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_scoreinfo — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" 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" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.photoinfo._photoinfo_scoreinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" PhotoInfo methods to expose computed score info from the library """</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">logging</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">.._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
|
||||
|
||||
|
||||
<span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">class</span> <span class="nc">ScoreInfo</span><span class="p">:</span>
|
||||
<span class="sd">""" Computed photo score info associated with a photo from the Photos library """</span>
|
||||
|
||||
<span class="n">overall</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">curation</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">promotion</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">highlight_visibility</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">behavioral</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">failure</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">harmonious_color</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">immersiveness</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">interaction</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">interesting_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">intrusive_object_presence</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">lively_color</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">low_light</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">noise</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_camera_tilt</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_composition</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_lighting</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_pattern</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_perspective</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_post_processing</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_reflection</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">pleasant_symmetry</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">sharply_focused_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<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>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">score</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" Computed score information for a photo</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ScoreInfo instance</span>
|
||||
<span class="sd"> """</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"score not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</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">_scoreinfo</span> <span class="c1"># pylint: disable=access-member-before-definition</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">scores</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">_db_scoreinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span> <span class="o">=</span> <span class="n">ScoreInfo</span><span class="p">(</span>
|
||||
<span class="n">overall</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"overall_aesthetic"</span><span class="p">],</span>
|
||||
<span class="n">curation</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"curation"</span><span class="p">],</span>
|
||||
<span class="n">promotion</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"promotion"</span><span class="p">],</span>
|
||||
<span class="n">highlight_visibility</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"highlight_visibility"</span><span class="p">],</span>
|
||||
<span class="n">behavioral</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"behavioral"</span><span class="p">],</span>
|
||||
<span class="n">failure</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"failure"</span><span class="p">],</span>
|
||||
<span class="n">harmonious_color</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"harmonious_color"</span><span class="p">],</span>
|
||||
<span class="n">immersiveness</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"immersiveness"</span><span class="p">],</span>
|
||||
<span class="n">interaction</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"interaction"</span><span class="p">],</span>
|
||||
<span class="n">interesting_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"interesting_subject"</span><span class="p">],</span>
|
||||
<span class="n">intrusive_object_presence</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"intrusive_object_presence"</span><span class="p">],</span>
|
||||
<span class="n">lively_color</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"lively_color"</span><span class="p">],</span>
|
||||
<span class="n">low_light</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"low_light"</span><span class="p">],</span>
|
||||
<span class="n">noise</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"noise"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_camera_tilt</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_camera_tilt"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_composition</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_composition"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_lighting</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_lighting"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_pattern</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_pattern"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_perspective</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_perspective"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_post_processing</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_post_processing"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_reflection</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_reflection"</span><span class="p">],</span>
|
||||
<span class="n">pleasant_symmetry</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"pleasant_symmetry"</span><span class="p">],</span>
|
||||
<span class="n">sharply_focused_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"sharply_focused_subject"</span><span class="p">],</span>
|
||||
<span class="n">tastefully_blurred</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"tastefully_blurred"</span><span class="p">],</span>
|
||||
<span class="n">well_chosen_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"well_chosen_subject"</span><span class="p">],</span>
|
||||
<span class="n">well_framed_subject</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"well_framed_subject"</span><span class="p">],</span>
|
||||
<span class="n">well_timed_shot</span><span class="o">=</span><span class="n">scores</span><span class="p">[</span><span class="s2">"well_timed_shot"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span> <span class="o">=</span> <span class="n">ScoreInfo</span><span class="p">(</span>
|
||||
<span class="n">overall</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">curation</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">promotion</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">highlight_visibility</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">behavioral</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">failure</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">harmonious_color</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">immersiveness</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">interaction</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">interesting_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">intrusive_object_presence</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">lively_color</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">low_light</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">noise</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_camera_tilt</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_composition</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_lighting</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_pattern</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_perspective</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_post_processing</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_reflection</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">pleasant_symmetry</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">sharply_focused_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">tastefully_blurred</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">well_chosen_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">well_framed_subject</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="n">well_timed_shot</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_scoreinfo</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</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="../../../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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
378
docs/_modules/osxphotos/photoinfo/_photoinfo_searchinfo.html
Normal file
@@ -0,0 +1,378 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_searchinfo — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" 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" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.photoinfo._photoinfo_searchinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" Methods and class for PhotoInfo exposing SearchInfo data such as labels </span>
|
||||
<span class="sd"> Adds the following properties to PhotoInfo (valid only for Photos 5):</span>
|
||||
<span class="sd"> search_info: returns a SearchInfo object</span>
|
||||
<span class="sd"> search_info_normalized: returns a SearchInfo object with properties that produce normalized results</span>
|
||||
<span class="sd"> labels: returns list of labels</span>
|
||||
<span class="sd"> labels_normalized: returns list of normalized labels</span>
|
||||
<span class="sd">"""</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_PHOTOS_4_VERSION</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_CITY</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_LABEL</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_NEIGHBORHOOD</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_PLACE_NAME</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_STREET</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_ALL_LOCALITY</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_COUNTRY</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_STATE</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_STATE_ABBREVIATION</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_BODY_OF_WATER</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_MONTH</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_YEAR</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_HOLIDAY</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_ACTIVITY</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_SEASON</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_VENUE</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_VENUE_TYPE</span><span class="p">,</span>
|
||||
<span class="n">SEARCH_CATEGORY_MEDIA_TYPES</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">search_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns SearchInfo object for photo </span>
|
||||
<span class="sd"> only valid on Photos 5, on older libraries, returns None</span>
|
||||
<span class="sd"> """</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="kc">None</span>
|
||||
|
||||
<span class="c1"># memoize SearchInfo object</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">_search_info</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">_search_info</span> <span class="o">=</span> <span class="n">SearchInfo</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_search_info</span>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">search_info_normalized</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns SearchInfo object for photo that produces normalized results</span>
|
||||
<span class="sd"> only valid on Photos 5, on older libraries, returns None</span>
|
||||
<span class="sd"> """</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="kc">None</span>
|
||||
|
||||
<span class="c1"># memoize SearchInfo object</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">_search_info_normalized</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">_search_info_normalized</span> <span class="o">=</span> <span class="n">SearchInfo</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_search_info_normalized</span>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">labels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of labels applied to photo by Photos image categorization</span>
|
||||
<span class="sd"> only valid on Photos 5, on older libraries returns empty list </span>
|
||||
<span class="sd"> """</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="p">[]</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info</span><span class="o">.</span><span class="n">labels</span>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">labels_normalized</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns normalized list of labels applied to photo by Photos image categorization</span>
|
||||
<span class="sd"> only valid on Photos 5, on older libraries returns empty list </span>
|
||||
<span class="sd"> """</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="p">[]</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">search_info_normalized</span><span class="o">.</span><span class="n">labels</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">SearchInfo</span><span class="p">:</span>
|
||||
<span class="sd">""" Info about search terms such as machine learning labels that Photos knows about a photo """</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">photo</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">""" photo: PhotoInfo object</span>
|
||||
<span class="sd"> normalized: if True, all properties return normalized (lower case) results """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">photo</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">NotImplementedError</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"search info not implemented for this database version"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="o">=</span> <span class="n">photo</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_normalized</span> <span class="o">=</span> <span class="n">normalized</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">uuid</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="c1"># get search info for this UUID</span>
|
||||
<span class="c1"># there might not be any search info data (e.g. if Photo was missing or photoanalysisd not run yet)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_searchinfo_uuid</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">labels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" return list of labels associated with Photo """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_LABEL</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">place_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of place names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_PLACE_NAME</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">streets</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of street names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STREET</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">neighborhoods</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of neighborhoods """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_NEIGHBORHOOD</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">locality_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of other locality names """</span>
|
||||
<span class="n">locality</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">category</span> <span class="ow">in</span> <span class="n">SEARCH_CATEGORY_ALL_LOCALITY</span><span class="p">:</span>
|
||||
<span class="n">locality</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">category</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">locality</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">city</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns city/town """</span>
|
||||
<span class="n">city</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_CITY</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">city</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">city</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">state</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns state name """</span>
|
||||
<span class="n">state</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STATE</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">state</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">state_abbreviation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns state abbreviation """</span>
|
||||
<span class="n">abbrev</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_STATE_ABBREVIATION</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">abbrev</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">abbrev</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">country</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns country name """</span>
|
||||
<span class="n">country</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_COUNTRY</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">country</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">country</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">month</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns month name """</span>
|
||||
<span class="n">month</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_MONTH</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">month</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">month</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">year</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns year """</span>
|
||||
<span class="n">year</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_YEAR</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">year</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">year</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">bodies_of_water</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of body of water names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_BODY_OF_WATER</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">holidays</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of holiday names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_HOLIDAY</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">activities</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of activity names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_ACTIVITY</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">season</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns season name """</span>
|
||||
<span class="n">season</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_SEASON</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">season</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">season</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">venues</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of venue names """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_VENUE</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">venue_types</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of venue types """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">SEARCH_CATEGORY_VENUE_TYPE</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">media_types</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" returns list of media types (photo, video, panorama, etc) """</span>
|
||||
<span class="n">types</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">category</span> <span class="ow">in</span> <span class="n">SEARCH_CATEGORY_MEDIA_TYPES</span><span class="p">:</span>
|
||||
<span class="n">types</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="n">category</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">types</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">all</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" return all search info properties in a single list """</span>
|
||||
<span class="nb">all</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">labels</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">place_names</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">streets</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">neighborhoods</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">locality_names</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">bodies_of_water</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">holidays</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">activities</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">venues</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">venue_types</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">media_types</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">]</span>
|
||||
|
||||
<span class="k">return</span> <span class="nb">all</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="sd">""" return dict of search info """</span>
|
||||
<span class="k">return</span> <span class="p">{</span>
|
||||
<span class="s2">"labels"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">labels</span><span class="p">,</span>
|
||||
<span class="s2">"place_names"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">place_names</span><span class="p">,</span>
|
||||
<span class="s2">"streets"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">streets</span><span class="p">,</span>
|
||||
<span class="s2">"neighborhoods"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">neighborhoods</span><span class="p">,</span>
|
||||
<span class="s2">"city"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">,</span>
|
||||
<span class="s2">"locality_names"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">locality_names</span><span class="p">,</span>
|
||||
<span class="s2">"state"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">,</span>
|
||||
<span class="s2">"state_abbreviation"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">,</span>
|
||||
<span class="s2">"country"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">,</span>
|
||||
<span class="s2">"bodies_of_water"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">bodies_of_water</span><span class="p">,</span>
|
||||
<span class="s2">"month"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">,</span>
|
||||
<span class="s2">"year"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">,</span>
|
||||
<span class="s2">"holidays"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">holidays</span><span class="p">,</span>
|
||||
<span class="s2">"activities"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">activities</span><span class="p">,</span>
|
||||
<span class="s2">"season"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">,</span>
|
||||
<span class="s2">"venues"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">venues</span><span class="p">,</span>
|
||||
<span class="s2">"venue_types"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">venue_types</span><span class="p">,</span>
|
||||
<span class="s2">"media_types"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">media_types</span><span class="p">,</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">category</span><span class="p">):</span>
|
||||
<span class="sd">""" return list of text for a specified category ID """</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span><span class="p">:</span>
|
||||
<span class="n">content</span> <span class="o">=</span> <span class="s2">"normalized_string"</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_normalized</span> <span class="k">else</span> <span class="s2">"content_string"</span>
|
||||
<span class="k">return</span> <span class="p">[</span>
|
||||
<span class="n">rec</span><span class="p">[</span><span class="n">content</span><span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">rec</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_searchinfo</span>
|
||||
<span class="k">if</span> <span class="n">rec</span><span class="p">[</span><span class="s2">"category"</span><span class="p">]</span> <span class="o">==</span> <span class="n">category</span>
|
||||
<span class="p">]</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</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="../../../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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1196
docs/_modules/osxphotos/photoinfo/photoinfo.html
Normal file
2963
docs/_modules/osxphotos/photosdb/photosdb.html
Normal file
6
docs/_sources/cli.rst.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
osxphotos command line interface (CLI)
|
||||
======================================
|
||||
|
||||
.. click:: osxphotos.cli:cli
|
||||
:prog: osxphotos
|
||||
:nested: full
|
||||
23
docs/_sources/index.rst.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
.. osxphotos documentation master file, created by
|
||||
sphinx-quickstart on Sat Jan 23 13:27:27 2021.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to osxphotos's documentation!
|
||||
=====================================
|
||||
|
||||
.. include:: ../../README.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
cli
|
||||
reference
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
5
docs/_sources/modules.rst.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
osxphotos
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
13
docs/_sources/reference.rst.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
osxphotos package
|
||||
===================
|
||||
|
||||
osxphotos module
|
||||
------------------------------
|
||||
|
||||
.. autoclass:: osxphotos.PhotosDB
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autoclass:: osxphotos.PhotoInfo
|
||||
:members:
|
||||
:undoc-members:
|
||||
701
docs/_static/alabaster.css
vendored
Normal file
@@ -0,0 +1,701 @@
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 17px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
div.document {
|
||||
width: 940px;
|
||||
margin: 30px auto 0 auto;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 220px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
width: 220px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #fff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 0 30px;
|
||||
}
|
||||
|
||||
div.body > .section {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
width: 940px;
|
||||
margin: 20px auto 30px auto;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
p.caption {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
|
||||
div.relations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a:hover {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 18px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.logo {
|
||||
padding: 0;
|
||||
margin: -10px 0 0 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper h1.logo {
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper h1.logo-name {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.blurb {
|
||||
margin-top: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: Georgia, serif;
|
||||
color: #444;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo a,
|
||||
div.sphinxsidebar h3 a,
|
||||
div.sphinxsidebar p.logo a:hover,
|
||||
div.sphinxsidebar h3 a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #555;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul li.toctree-l1 > a {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul li.toctree-l2 > a {
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #CCC;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
color: #AAA;
|
||||
background: #AAA;
|
||||
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar .badge {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar .badge:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* To address an issue with donation coming after search */
|
||||
div.sphinxsidebar h3.donation {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #004B6B;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #6D4100;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: normal;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
||||
div.body h2 { font-size: 180%; }
|
||||
div.body h3 { font-size: 150%; }
|
||||
div.body h4 { font-size: 130%; }
|
||||
div.body h5 { font-size: 100%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: #DDD;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
color: #444;
|
||||
background: #EAEAEA;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
div.admonition {
|
||||
margin: 20px 0px;
|
||||
padding: 10px 30px;
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
div.admonition p.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.highlight {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background: #FAF3E8;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.danger {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
-moz-box-shadow: 2px 2px 4px #D52C2C;
|
||||
-webkit-box-shadow: 2px 2px 4px #D52C2C;
|
||||
box-shadow: 2px 2px 4px #D52C2C;
|
||||
}
|
||||
|
||||
div.error {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
-moz-box-shadow: 2px 2px 4px #D52C2C;
|
||||
-webkit-box-shadow: 2px 2px 4px #D52C2C;
|
||||
box-shadow: 2px 2px 4px #D52C2C;
|
||||
}
|
||||
|
||||
div.caution {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.attention {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.important {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.tip {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.hint {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre, tt, code {
|
||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.hll {
|
||||
background-color: #FFC;
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname, code.descname, code.descclassname {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
tt.descname, code.descname {
|
||||
padding-right: 0.08em;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
-moz-box-shadow: 2px 2px 4px #EEE;
|
||||
-webkit-box-shadow: 2px 2px 4px #EEE;
|
||||
box-shadow: 2px 2px 4px #EEE;
|
||||
}
|
||||
|
||||
table.docutils {
|
||||
border: 1px solid #888;
|
||||
-moz-box-shadow: 2px 2px 4px #EEE;
|
||||
-webkit-box-shadow: 2px 2px 4px #EEE;
|
||||
box-shadow: 2px 2px 4px #EEE;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
border: 1px solid #888;
|
||||
padding: 0.25em 0.7em;
|
||||
}
|
||||
|
||||
table.field-list, table.footnote {
|
||||
border: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
table.footnote {
|
||||
margin: 15px 0;
|
||||
width: 100%;
|
||||
border: 1px solid #EEE;
|
||||
background: #FDFDFD;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
table.footnote + table.footnote {
|
||||
margin-top: -15px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.field-list th {
|
||||
padding: 0 0.8em 0 0;
|
||||
}
|
||||
|
||||
table.field-list td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.field-list p {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
/* Cloned from
|
||||
* https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
|
||||
*/
|
||||
.field-name {
|
||||
-moz-hyphens: manual;
|
||||
-ms-hyphens: manual;
|
||||
-webkit-hyphens: manual;
|
||||
hyphens: manual;
|
||||
}
|
||||
|
||||
table.footnote td.label {
|
||||
width: .1px;
|
||||
padding: 0.3em 0 0.3em 0.5em;
|
||||
}
|
||||
|
||||
table.footnote td {
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 0 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
/* Matches the 30px from the narrow-screen "li > ul" selector below */
|
||||
margin: 10px 0 10px 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #EEE;
|
||||
padding: 7px 30px;
|
||||
margin: 15px 0px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background: #ffd;
|
||||
}
|
||||
|
||||
dl pre, blockquote pre, li pre {
|
||||
margin-left: 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
tt, code {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
}
|
||||
|
||||
tt.xref, code.xref, a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
|
||||
a.reference {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #004B6B;
|
||||
}
|
||||
|
||||
/* Don't put an underline on images */
|
||||
a.image-reference, a.image-reference:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a.reference:hover {
|
||||
border-bottom: 1px solid #6D4100;
|
||||
}
|
||||
|
||||
a.footnote-reference {
|
||||
text-decoration: none;
|
||||
font-size: 0.7em;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px dotted #004B6B;
|
||||
}
|
||||
|
||||
a.footnote-reference:hover {
|
||||
border-bottom: 1px solid #6D4100;
|
||||
}
|
||||
|
||||
a:hover tt, a:hover code {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 870px) {
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
li > ul {
|
||||
/* Matches the 30px from the "ul, ol" selector above */
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 875px) {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 102.5%;
|
||||
margin: 50px -30px -20px -30px;
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.body {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rtd_doc_footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* misc. */
|
||||
|
||||
.revsys-inline {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
/* Make nested-list/multi-paragraph items look better in Releases changelog
|
||||
* pages. Without this, docutils' magical list fuckery causes inconsistent
|
||||
* formatting between different release sub-lists.
|
||||
*/
|
||||
div#changelog > div.section > ul > li > p:only-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Hide fugly table cell borders in ..bibliography:: directive output */
|
||||
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
|
||||
border: none;
|
||||
/* Below needed in some edge cases; if not applied, bottom shadows appear */
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
/* relbar */
|
||||
|
||||
.related {
|
||||
line-height: 30px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.related.top {
|
||||
border-bottom: 1px solid #EEE;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.related.bottom {
|
||||
border-top: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.related ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.related li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
nav#rellinks {
|
||||
float: right;
|
||||
}
|
||||
|
||||
nav#rellinks li+li:before {
|
||||
content: "|";
|
||||
}
|
||||
|
||||
nav#breadcrumbs li+li:before {
|
||||
content: "\00BB";
|
||||
}
|
||||
|
||||
/* Hide certain items when printing */
|
||||
@media print {
|
||||
div.related {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
856
docs/_static/basic.css
vendored
Normal file
@@ -0,0 +1,856 @@
|
||||
/*
|
||||
* basic.css
|
||||
* ~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- basic theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
/* -- main layout ----------------------------------------------------------- */
|
||||
|
||||
div.clearer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div.section::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* -- relbar ---------------------------------------------------------------- */
|
||||
|
||||
div.related {
|
||||
width: 100%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
div.related h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.related li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.related li.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* -- sidebar --------------------------------------------------------------- */
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 10px 5px 0 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
float: left;
|
||||
width: 230px;
|
||||
margin-left: -100%;
|
||||
font-size: 90%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap : break-word;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul,
|
||||
div.sphinxsidebar ul.want-points {
|
||||
margin-left: 20px;
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #98dbcc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox form.search {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox input[type="text"] {
|
||||
float: left;
|
||||
width: 80%;
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox input[type="submit"] {
|
||||
float: left;
|
||||
width: 20%;
|
||||
border-left: none;
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* -- search page ----------------------------------------------------------- */
|
||||
|
||||
ul.search {
|
||||
margin: 10px 0 0 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.search li {
|
||||
padding: 5px 0 5px 20px;
|
||||
background-image: url(file.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 7px;
|
||||
}
|
||||
|
||||
ul.search li a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.search li div.context {
|
||||
color: #888;
|
||||
margin: 2px 0 0 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul.keywordmatches li.goodmatch a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- index page ------------------------------------------------------------ */
|
||||
|
||||
table.contentstable {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.contentstable p.biglink {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a.biglink {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.linkdescr {
|
||||
font-style: italic;
|
||||
padding-top: 5px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* -- general index --------------------------------------------------------- */
|
||||
|
||||
table.indextable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.indextable td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.indextable ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
table.indextable > tbody > tr > td > ul {
|
||||
padding-left: 0em;
|
||||
}
|
||||
|
||||
table.indextable tr.pcap {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
table.indextable tr.cap {
|
||||
margin-top: 10px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
img.toggler {
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.modindex-jumpbox {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 1em 0 1em 0;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
div.genindex-jumpbox {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 1em 0 1em 0;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
/* -- domain module index --------------------------------------------------- */
|
||||
|
||||
table.modindextable td {
|
||||
padding: 2px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* -- general body styles --------------------------------------------------- */
|
||||
|
||||
div.body {
|
||||
min-width: 450px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li, div.body blockquote {
|
||||
-moz-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
a.brackets:before,
|
||||
span.brackets > a:before{
|
||||
content: "[";
|
||||
}
|
||||
|
||||
a.brackets:after,
|
||||
span.brackets > a:after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
h1:hover > a.headerlink,
|
||||
h2:hover > a.headerlink,
|
||||
h3:hover > a.headerlink,
|
||||
h4:hover > a.headerlink,
|
||||
h5:hover > a.headerlink,
|
||||
h6:hover > a.headerlink,
|
||||
dt:hover > a.headerlink,
|
||||
caption:hover > a.headerlink,
|
||||
p.caption:hover > a.headerlink,
|
||||
div.code-block-caption:hover > a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
div.body p.caption {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
div.body td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
p.rubric {
|
||||
margin-top: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left {
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right {
|
||||
clear: right;
|
||||
float: right;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
img.align-default, .figure.align-default {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* -- sidebars -------------------------------------------------------------- */
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em;
|
||||
border: 1px solid #ddb;
|
||||
padding: 7px;
|
||||
background-color: #ffe;
|
||||
width: 40%;
|
||||
float: right;
|
||||
clear: right;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.admonition, div.topic, blockquote {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* -- topics ---------------------------------------------------------------- */
|
||||
|
||||
div.topic {
|
||||
border: 1px solid #ccc;
|
||||
padding: 7px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
p.topic-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- admonitions ----------------------------------------------------------- */
|
||||
|
||||
div.admonition {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
div.admonition dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
margin: 0px 10px 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.body p.centered {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* -- content of sidebars/topics/admonitions -------------------------------- */
|
||||
|
||||
div.sidebar > :last-child,
|
||||
div.topic > :last-child,
|
||||
div.admonition > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sidebar::after,
|
||||
div.topic::after,
|
||||
div.admonition::after,
|
||||
blockquote::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -- tables ---------------------------------------------------------------- */
|
||||
|
||||
table.docutils {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-default {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table caption span.caption-number {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
table caption span.caption-text {
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
padding: 1px 8px 1px 5px;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
table.footnote td, table.footnote th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
table.citation td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
th > :first-child,
|
||||
td > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
th > :last-child,
|
||||
td > :last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* -- figures --------------------------------------------------------------- */
|
||||
|
||||
div.figure {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.figure p.caption {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-number {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-text {
|
||||
}
|
||||
|
||||
/* -- field list styles ----------------------------------------------------- */
|
||||
|
||||
table.field-list td, table.field-list th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.field-list ul {
|
||||
margin: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.field-list p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
-moz-hyphens: manual;
|
||||
-ms-hyphens: manual;
|
||||
-webkit-hyphens: manual;
|
||||
hyphens: manual;
|
||||
}
|
||||
|
||||
/* -- hlist styles ---------------------------------------------------------- */
|
||||
|
||||
table.hlist {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
table.hlist td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha;
|
||||
}
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha;
|
||||
}
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman;
|
||||
}
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman;
|
||||
}
|
||||
|
||||
:not(li) > ol > li:first-child > :first-child,
|
||||
:not(li) > ul > li:first-child > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
:not(li) > ol > li:last-child > :last-child,
|
||||
:not(li) > ul > li:last-child > :last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
ol.simple ol p,
|
||||
ol.simple ul p,
|
||||
ul.simple ol p,
|
||||
ul.simple ul p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.simple > li:not(:first-child) > p,
|
||||
ul.simple > li:not(:first-child) > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.simple p,
|
||||
ul.simple p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dl.footnote > dt,
|
||||
dl.citation > dt {
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
dl.footnote > dd,
|
||||
dl.citation > dd {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
dl.footnote > dd:after,
|
||||
dl.citation > dd:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
|
||||
dl.field-list {
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(30%) auto;
|
||||
}
|
||||
|
||||
dl.field-list > dt {
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
dl.field-list > dt:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
dl.field-list > dd {
|
||||
padding-left: 0.5em;
|
||||
margin-top: 0em;
|
||||
margin-left: 0em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
dd > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
dd ul, dd table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
dl > dd:last-child,
|
||||
dl > dd:last-child > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt:target, span.highlighted {
|
||||
background-color: #fbe54e;
|
||||
}
|
||||
|
||||
rect.highlighted {
|
||||
fill: #fbe54e;
|
||||
}
|
||||
|
||||
dl.glossary dt {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.optional {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.sig-paren {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.versionmodified {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
background-color: #fda;
|
||||
padding: 5px;
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
.footnote:target {
|
||||
background-color: #ffa;
|
||||
}
|
||||
|
||||
.line-block {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.line-block .line-block {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.guilabel, .menuselection {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.accelerator {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.classifier {
|
||||
font-style: oblique;
|
||||
}
|
||||
|
||||
.classifier:before {
|
||||
font-style: normal;
|
||||
margin: 0.5em;
|
||||
content: ":";
|
||||
}
|
||||
|
||||
abbr, acronym {
|
||||
border-bottom: dotted 1px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* -- code displays --------------------------------------------------------- */
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
overflow-y: hidden; /* fixes display issues on Chrome browsers */
|
||||
}
|
||||
|
||||
pre, div[class*="highlight-"] {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
div[class*="highlight-"] {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
table.highlighttable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.highlighttable tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.highlighttable tr {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
table.highlighttable td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.highlighttable td.linenos {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
table.highlighttable td.code {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.highlight .hll {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.highlight pre,
|
||||
table.highlighttable pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.code-block-caption + div {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
div.code-block-caption {
|
||||
margin-top: 1em;
|
||||
padding: 2px 5px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
div.code-block-caption code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
table.highlighttable td.linenos,
|
||||
span.linenos,
|
||||
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div.code-block-caption span.caption-number {
|
||||
padding: 0.1em 0.3em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.code-block-caption span.caption-text {
|
||||
}
|
||||
|
||||
div.literal-block-wrapper {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
code.descname {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
code.descclassname {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
code.xref, a code {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.viewcode-link {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
float: right;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
margin: -1px -10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* -- math display ---------------------------------------------------------- */
|
||||
|
||||
img.math {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.body div.math p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span.eqno {
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.eqno a.headerlink {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
div.math:hover a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* -- printout stylesheet --------------------------------------------------- */
|
||||
|
||||
@media print {
|
||||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar,
|
||||
div.related,
|
||||
div.footer,
|
||||
#top-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
1
docs/_static/custom.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/* This file intentionally left blank. */
|
||||
316
docs/_static/doctools.js
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* doctools.js
|
||||
* ~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx JavaScript utilities for all documentation.
|
||||
*
|
||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* select a different prefix for underscore
|
||||
*/
|
||||
$u = _.noConflict();
|
||||
|
||||
/**
|
||||
* make the code below compatible with browsers without
|
||||
* an installed firebug like debugger
|
||||
if (!window.console || !console.firebug) {
|
||||
var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
|
||||
"dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
|
||||
"profile", "profileEnd"];
|
||||
window.console = {};
|
||||
for (var i = 0; i < names.length; ++i)
|
||||
window.console[names[i]] = function() {};
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* small helper function to urldecode strings
|
||||
*/
|
||||
jQuery.urldecode = function(x) {
|
||||
return decodeURIComponent(x).replace(/\+/g, ' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* small helper function to urlencode strings
|
||||
*/
|
||||
jQuery.urlencode = encodeURIComponent;
|
||||
|
||||
/**
|
||||
* This function returns the parsed url parameters of the
|
||||
* current request. Multiple values per key are supported,
|
||||
* it will always return arrays of strings for the value parts.
|
||||
*/
|
||||
jQuery.getQueryParameters = function(s) {
|
||||
if (typeof s === 'undefined')
|
||||
s = document.location.search;
|
||||
var parts = s.substr(s.indexOf('?') + 1).split('&');
|
||||
var result = {};
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var tmp = parts[i].split('=', 2);
|
||||
var key = jQuery.urldecode(tmp[0]);
|
||||
var value = jQuery.urldecode(tmp[1]);
|
||||
if (key in result)
|
||||
result[key].push(value);
|
||||
else
|
||||
result[key] = [value];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* highlight a given string on a jquery object by wrapping it in
|
||||
* span elements with the given class name.
|
||||
*/
|
||||
jQuery.fn.highlightText = function(text, className) {
|
||||
function highlight(node, addItems) {
|
||||
if (node.nodeType === 3) {
|
||||
var val = node.nodeValue;
|
||||
var pos = val.toLowerCase().indexOf(text);
|
||||
if (pos >= 0 &&
|
||||
!jQuery(node.parentNode).hasClass(className) &&
|
||||
!jQuery(node.parentNode).hasClass("nohighlight")) {
|
||||
var span;
|
||||
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
|
||||
if (isInSVG) {
|
||||
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
} else {
|
||||
span = document.createElement("span");
|
||||
span.className = className;
|
||||
}
|
||||
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
|
||||
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
|
||||
document.createTextNode(val.substr(pos + text.length)),
|
||||
node.nextSibling));
|
||||
node.nodeValue = val.substr(0, pos);
|
||||
if (isInSVG) {
|
||||
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
var bbox = node.parentElement.getBBox();
|
||||
rect.x.baseVal.value = bbox.x;
|
||||
rect.y.baseVal.value = bbox.y;
|
||||
rect.width.baseVal.value = bbox.width;
|
||||
rect.height.baseVal.value = bbox.height;
|
||||
rect.setAttribute('class', className);
|
||||
addItems.push({
|
||||
"parent": node.parentNode,
|
||||
"target": rect});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!jQuery(node).is("button, select, textarea")) {
|
||||
jQuery.each(node.childNodes, function() {
|
||||
highlight(this, addItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
var addItems = [];
|
||||
var result = this.each(function() {
|
||||
highlight(this, addItems);
|
||||
});
|
||||
for (var i = 0; i < addItems.length; ++i) {
|
||||
jQuery(addItems[i].parent).before(addItems[i].target);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
* backward compatibility for jQuery.browser
|
||||
* This will be supported until firefox bug is fixed.
|
||||
*/
|
||||
if (!jQuery.browser) {
|
||||
jQuery.uaMatch = function(ua) {
|
||||
ua = ua.toLowerCase();
|
||||
|
||||
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(msie) ([\w.]+)/.exec(ua) ||
|
||||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
||||
[];
|
||||
|
||||
return {
|
||||
browser: match[ 1 ] || "",
|
||||
version: match[ 2 ] || "0"
|
||||
};
|
||||
};
|
||||
jQuery.browser = {};
|
||||
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small JavaScript module for the documentation.
|
||||
*/
|
||||
var Documentation = {
|
||||
|
||||
init : function() {
|
||||
this.fixFirefoxAnchorBug();
|
||||
this.highlightSearchWords();
|
||||
this.initIndexTable();
|
||||
if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
|
||||
this.initOnKeyListeners();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* i18n support
|
||||
*/
|
||||
TRANSLATIONS : {},
|
||||
PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
|
||||
LOCALE : 'unknown',
|
||||
|
||||
// gettext and ngettext don't access this so that the functions
|
||||
// can safely bound to a different name (_ = Documentation.gettext)
|
||||
gettext : function(string) {
|
||||
var translated = Documentation.TRANSLATIONS[string];
|
||||
if (typeof translated === 'undefined')
|
||||
return string;
|
||||
return (typeof translated === 'string') ? translated : translated[0];
|
||||
},
|
||||
|
||||
ngettext : function(singular, plural, n) {
|
||||
var translated = Documentation.TRANSLATIONS[singular];
|
||||
if (typeof translated === 'undefined')
|
||||
return (n == 1) ? singular : plural;
|
||||
return translated[Documentation.PLURALEXPR(n)];
|
||||
},
|
||||
|
||||
addTranslations : function(catalog) {
|
||||
for (var key in catalog.messages)
|
||||
this.TRANSLATIONS[key] = catalog.messages[key];
|
||||
this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
|
||||
this.LOCALE = catalog.locale;
|
||||
},
|
||||
|
||||
/**
|
||||
* add context elements like header anchor links
|
||||
*/
|
||||
addContextElements : function() {
|
||||
$('div[id] > :header:first').each(function() {
|
||||
$('<a class="headerlink">\u00B6</a>').
|
||||
attr('href', '#' + this.id).
|
||||
attr('title', _('Permalink to this headline')).
|
||||
appendTo(this);
|
||||
});
|
||||
$('dt[id]').each(function() {
|
||||
$('<a class="headerlink">\u00B6</a>').
|
||||
attr('href', '#' + this.id).
|
||||
attr('title', _('Permalink to this definition')).
|
||||
appendTo(this);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* workaround a firefox stupidity
|
||||
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
|
||||
*/
|
||||
fixFirefoxAnchorBug : function() {
|
||||
if (document.location.hash && $.browser.mozilla)
|
||||
window.setTimeout(function() {
|
||||
document.location.href += '';
|
||||
}, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* highlight the search words provided in the url in the text
|
||||
*/
|
||||
highlightSearchWords : function() {
|
||||
var params = $.getQueryParameters();
|
||||
var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
|
||||
if (terms.length) {
|
||||
var body = $('div.body');
|
||||
if (!body.length) {
|
||||
body = $('body');
|
||||
}
|
||||
window.setTimeout(function() {
|
||||
$.each(terms, function() {
|
||||
body.highlightText(this.toLowerCase(), 'highlighted');
|
||||
});
|
||||
}, 10);
|
||||
$('<p class="highlight-link"><a href="javascript:Documentation.' +
|
||||
'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
|
||||
.appendTo($('#searchbox'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* init the domain index toggle buttons
|
||||
*/
|
||||
initIndexTable : function() {
|
||||
var togglers = $('img.toggler').click(function() {
|
||||
var src = $(this).attr('src');
|
||||
var idnum = $(this).attr('id').substr(7);
|
||||
$('tr.cg-' + idnum).toggle();
|
||||
if (src.substr(-9) === 'minus.png')
|
||||
$(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
|
||||
else
|
||||
$(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
|
||||
}).css('display', '');
|
||||
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
|
||||
togglers.click();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* helper function to hide the search marks again
|
||||
*/
|
||||
hideSearchWords : function() {
|
||||
$('#searchbox .highlight-link').fadeOut(300);
|
||||
$('span.highlighted').removeClass('highlighted');
|
||||
},
|
||||
|
||||
/**
|
||||
* make the url absolute
|
||||
*/
|
||||
makeURL : function(relativeURL) {
|
||||
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
|
||||
},
|
||||
|
||||
/**
|
||||
* get the current relative url
|
||||
*/
|
||||
getCurrentURL : function() {
|
||||
var path = document.location.pathname;
|
||||
var parts = path.split(/\//);
|
||||
$.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
|
||||
if (this === '..')
|
||||
parts.pop();
|
||||
});
|
||||
var url = parts.join('/');
|
||||
return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
|
||||
},
|
||||
|
||||
initOnKeyListeners: function() {
|
||||
$(document).keydown(function(event) {
|
||||
var activeElementType = document.activeElement.tagName;
|
||||
// don't navigate when in search box, textarea, dropdown or button
|
||||
if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
|
||||
&& activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
|
||||
&& !event.shiftKey) {
|
||||
switch (event.keyCode) {
|
||||
case 37: // left
|
||||
var prevHref = $('link[rel="prev"]').prop('href');
|
||||
if (prevHref) {
|
||||
window.location.href = prevHref;
|
||||
return false;
|
||||
}
|
||||
case 39: // right
|
||||
var nextHref = $('link[rel="next"]').prop('href');
|
||||
if (nextHref) {
|
||||
window.location.href = nextHref;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// quick alias for translations
|
||||
_ = Documentation.gettext;
|
||||
|
||||
$(document).ready(function() {
|
||||
Documentation.init();
|
||||
});
|
||||
12
docs/_static/documentation_options.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.40.8',
|
||||
LANGUAGE: 'None',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
FILE_SUFFIX: '.html',
|
||||
LINK_SUFFIX: '.html',
|
||||
HAS_SOURCE: true,
|
||||
SOURCELINK_SUFFIX: '.txt',
|
||||
NAVIGATION_WITH_KEYS: false
|
||||
};
|
||||
BIN
docs/_static/file.png
vendored
Normal file
|
After Width: | Height: | Size: 286 B |
10872
docs/_static/jquery-3.5.1.js
vendored
Normal file
2
docs/_static/jquery.js
vendored
Normal file
297
docs/_static/language_data.js
vendored
Normal file
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* language_data.js
|
||||
* ~~~~~~~~~~~~~~~~
|
||||
*
|
||||
* This script contains the language-specific data used by searchtools.js,
|
||||
* namely the list of stopwords, stemmer, scorer and splitter.
|
||||
*
|
||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
|
||||
|
||||
|
||||
/* Non-minified version JS is _stemmer.js if file is provided */
|
||||
/**
|
||||
* Porter Stemmer
|
||||
*/
|
||||
var Stemmer = function() {
|
||||
|
||||
var step2list = {
|
||||
ational: 'ate',
|
||||
tional: 'tion',
|
||||
enci: 'ence',
|
||||
anci: 'ance',
|
||||
izer: 'ize',
|
||||
bli: 'ble',
|
||||
alli: 'al',
|
||||
entli: 'ent',
|
||||
eli: 'e',
|
||||
ousli: 'ous',
|
||||
ization: 'ize',
|
||||
ation: 'ate',
|
||||
ator: 'ate',
|
||||
alism: 'al',
|
||||
iveness: 'ive',
|
||||
fulness: 'ful',
|
||||
ousness: 'ous',
|
||||
aliti: 'al',
|
||||
iviti: 'ive',
|
||||
biliti: 'ble',
|
||||
logi: 'log'
|
||||
};
|
||||
|
||||
var step3list = {
|
||||
icate: 'ic',
|
||||
ative: '',
|
||||
alize: 'al',
|
||||
iciti: 'ic',
|
||||
ical: 'ic',
|
||||
ful: '',
|
||||
ness: ''
|
||||
};
|
||||
|
||||
var c = "[^aeiou]"; // consonant
|
||||
var v = "[aeiouy]"; // vowel
|
||||
var C = c + "[^aeiouy]*"; // consonant sequence
|
||||
var V = v + "[aeiou]*"; // vowel sequence
|
||||
|
||||
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
|
||||
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
|
||||
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
|
||||
var s_v = "^(" + C + ")?" + v; // vowel in stem
|
||||
|
||||
this.stemWord = function (w) {
|
||||
var stem;
|
||||
var suffix;
|
||||
var firstch;
|
||||
var origword = w;
|
||||
|
||||
if (w.length < 3)
|
||||
return w;
|
||||
|
||||
var re;
|
||||
var re2;
|
||||
var re3;
|
||||
var re4;
|
||||
|
||||
firstch = w.substr(0,1);
|
||||
if (firstch == "y")
|
||||
w = firstch.toUpperCase() + w.substr(1);
|
||||
|
||||
// Step 1a
|
||||
re = /^(.+?)(ss|i)es$/;
|
||||
re2 = /^(.+?)([^s])s$/;
|
||||
|
||||
if (re.test(w))
|
||||
w = w.replace(re,"$1$2");
|
||||
else if (re2.test(w))
|
||||
w = w.replace(re2,"$1$2");
|
||||
|
||||
// Step 1b
|
||||
re = /^(.+?)eed$/;
|
||||
re2 = /^(.+?)(ed|ing)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(fp[1])) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
}
|
||||
else if (re2.test(w)) {
|
||||
var fp = re2.exec(w);
|
||||
stem = fp[1];
|
||||
re2 = new RegExp(s_v);
|
||||
if (re2.test(stem)) {
|
||||
w = stem;
|
||||
re2 = /(at|bl|iz)$/;
|
||||
re3 = new RegExp("([^aeiouylsz])\\1$");
|
||||
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
||||
if (re2.test(w))
|
||||
w = w + "e";
|
||||
else if (re3.test(w)) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
else if (re4.test(w))
|
||||
w = w + "e";
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1c
|
||||
re = /^(.+?)y$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(s_v);
|
||||
if (re.test(stem))
|
||||
w = stem + "i";
|
||||
}
|
||||
|
||||
// Step 2
|
||||
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
suffix = fp[2];
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(stem))
|
||||
w = stem + step2list[suffix];
|
||||
}
|
||||
|
||||
// Step 3
|
||||
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
suffix = fp[2];
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(stem))
|
||||
w = stem + step3list[suffix];
|
||||
}
|
||||
|
||||
// Step 4
|
||||
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
|
||||
re2 = /^(.+?)(s|t)(ion)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(mgr1);
|
||||
if (re.test(stem))
|
||||
w = stem;
|
||||
}
|
||||
else if (re2.test(w)) {
|
||||
var fp = re2.exec(w);
|
||||
stem = fp[1] + fp[2];
|
||||
re2 = new RegExp(mgr1);
|
||||
if (re2.test(stem))
|
||||
w = stem;
|
||||
}
|
||||
|
||||
// Step 5
|
||||
re = /^(.+?)e$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(mgr1);
|
||||
re2 = new RegExp(meq1);
|
||||
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
||||
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
|
||||
w = stem;
|
||||
}
|
||||
re = /ll$/;
|
||||
re2 = new RegExp(mgr1);
|
||||
if (re.test(w) && re2.test(w)) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
|
||||
// and turn initial Y back to y
|
||||
if (firstch == "y")
|
||||
w = firstch.toLowerCase() + w.substr(1);
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var splitChars = (function() {
|
||||
var result = {};
|
||||
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
|
||||
1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,
|
||||
2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,
|
||||
2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,
|
||||
3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,
|
||||
3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,
|
||||
4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,
|
||||
8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,
|
||||
11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,
|
||||
43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];
|
||||
var i, j, start, end;
|
||||
for (i = 0; i < singles.length; i++) {
|
||||
result[singles[i]] = true;
|
||||
}
|
||||
var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],
|
||||
[722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],
|
||||
[1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],
|
||||
[1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],
|
||||
[1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],
|
||||
[2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],
|
||||
[2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],
|
||||
[2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],
|
||||
[2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],
|
||||
[2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],
|
||||
[2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],
|
||||
[2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],
|
||||
[3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],
|
||||
[3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],
|
||||
[3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],
|
||||
[3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],
|
||||
[3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],
|
||||
[3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],
|
||||
[4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],
|
||||
[4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],
|
||||
[4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],
|
||||
[4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],
|
||||
[5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],
|
||||
[6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],
|
||||
[6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],
|
||||
[6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],
|
||||
[6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],
|
||||
[7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],
|
||||
[7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],
|
||||
[8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],
|
||||
[8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],
|
||||
[8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],
|
||||
[10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],
|
||||
[11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],
|
||||
[12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],
|
||||
[12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],
|
||||
[12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],
|
||||
[19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],
|
||||
[42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],
|
||||
[42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],
|
||||
[43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],
|
||||
[43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],
|
||||
[43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],
|
||||
[43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],
|
||||
[44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],
|
||||
[57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],
|
||||
[64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],
|
||||
[65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],
|
||||
[65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];
|
||||
for (i = 0; i < ranges.length; i++) {
|
||||
start = ranges[i][0];
|
||||
end = ranges[i][1];
|
||||
for (j = start; j <= end; j++) {
|
||||
result[j] = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
function splitQuery(query) {
|
||||
var result = [];
|
||||
var start = -1;
|
||||
for (var i = 0; i < query.length; i++) {
|
||||
if (splitChars[query.charCodeAt(i)]) {
|
||||
if (start !== -1) {
|
||||
result.push(query.slice(start, i));
|
||||
start = -1;
|
||||
}
|
||||
} else if (start === -1) {
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
if (start !== -1) {
|
||||
result.push(query.slice(start));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
BIN
docs/_static/minus.png
vendored
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
docs/_static/plus.png
vendored
Normal file
|
After Width: | Height: | Size: 90 B |
82
docs/_static/pygments.css
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
|
||||
.highlight .g { color: #000000 } /* Generic */
|
||||
.highlight .k { color: #004461; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000000 } /* Literal */
|
||||
.highlight .n { color: #000000 } /* Name */
|
||||
.highlight .o { color: #582800 } /* Operator */
|
||||
.highlight .x { color: #000000 } /* Other */
|
||||
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8f5902 } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #a40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #ef2929 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #888888 } /* Generic.Output */
|
||||
.highlight .gp { color: #745334 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000000 } /* Literal.Date */
|
||||
.highlight .m { color: #990000 } /* Literal.Number */
|
||||
.highlight .s { color: #4e9a06 } /* Literal.String */
|
||||
.highlight .na { color: #c4a000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #004461 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000000 } /* Name.Class */
|
||||
.highlight .no { color: #000000 } /* Name.Constant */
|
||||
.highlight .nd { color: #888888 } /* Name.Decorator */
|
||||
.highlight .ni { color: #ce5c00 } /* Name.Entity */
|
||||
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000000 } /* Name.Function */
|
||||
.highlight .nl { color: #f57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000000 } /* Name.Other */
|
||||
.highlight .py { color: #000000 } /* Name.Property */
|
||||
.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000000 } /* Name.Variable */
|
||||
.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */
|
||||
.highlight .mb { color: #990000 } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #990000 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #990000 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #990000 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #990000 } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */
|
||||
514
docs/_static/searchtools.js
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
/*
|
||||
* searchtools.js
|
||||
* ~~~~~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx JavaScript utilities for the full-text search.
|
||||
*
|
||||
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
if (!Scorer) {
|
||||
/**
|
||||
* Simple result scoring code.
|
||||
*/
|
||||
var Scorer = {
|
||||
// Implement the following function to further tweak the score for each result
|
||||
// The function takes a result array [filename, title, anchor, descr, score]
|
||||
// and returns the new score.
|
||||
/*
|
||||
score: function(result) {
|
||||
return result[4];
|
||||
},
|
||||
*/
|
||||
|
||||
// query matches the full name of an object
|
||||
objNameMatch: 11,
|
||||
// or matches in the last dotted part of the object name
|
||||
objPartialMatch: 6,
|
||||
// Additive scores depending on the priority of the object
|
||||
objPrio: {0: 15, // used to be importantResults
|
||||
1: 5, // used to be objectResults
|
||||
2: -5}, // used to be unimportantResults
|
||||
// Used when the priority is not in the mapping.
|
||||
objPrioDefault: 0,
|
||||
|
||||
// query found in title
|
||||
title: 15,
|
||||
partialTitle: 7,
|
||||
// query found in terms
|
||||
term: 5,
|
||||
partialTerm: 2
|
||||
};
|
||||
}
|
||||
|
||||
if (!splitQuery) {
|
||||
function splitQuery(query) {
|
||||
return query.split(/\s+/);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Module
|
||||
*/
|
||||
var Search = {
|
||||
|
||||
_index : null,
|
||||
_queued_query : null,
|
||||
_pulse_status : -1,
|
||||
|
||||
htmlToText : function(htmlString) {
|
||||
var virtualDocument = document.implementation.createHTMLDocument('virtual');
|
||||
var htmlElement = $(htmlString, virtualDocument);
|
||||
htmlElement.find('.headerlink').remove();
|
||||
docContent = htmlElement.find('[role=main]')[0];
|
||||
if(docContent === undefined) {
|
||||
console.warn("Content block not found. Sphinx search tries to obtain it " +
|
||||
"via '[role=main]'. Could you check your theme or template.");
|
||||
return "";
|
||||
}
|
||||
return docContent.textContent || docContent.innerText;
|
||||
},
|
||||
|
||||
init : function() {
|
||||
var params = $.getQueryParameters();
|
||||
if (params.q) {
|
||||
var query = params.q[0];
|
||||
$('input[name="q"]')[0].value = query;
|
||||
this.performSearch(query);
|
||||
}
|
||||
},
|
||||
|
||||
loadIndex : function(url) {
|
||||
$.ajax({type: "GET", url: url, data: null,
|
||||
dataType: "script", cache: true,
|
||||
complete: function(jqxhr, textstatus) {
|
||||
if (textstatus != "success") {
|
||||
document.getElementById("searchindexloader").src = url;
|
||||
}
|
||||
}});
|
||||
},
|
||||
|
||||
setIndex : function(index) {
|
||||
var q;
|
||||
this._index = index;
|
||||
if ((q = this._queued_query) !== null) {
|
||||
this._queued_query = null;
|
||||
Search.query(q);
|
||||
}
|
||||
},
|
||||
|
||||
hasIndex : function() {
|
||||
return this._index !== null;
|
||||
},
|
||||
|
||||
deferQuery : function(query) {
|
||||
this._queued_query = query;
|
||||
},
|
||||
|
||||
stopPulse : function() {
|
||||
this._pulse_status = 0;
|
||||
},
|
||||
|
||||
startPulse : function() {
|
||||
if (this._pulse_status >= 0)
|
||||
return;
|
||||
function pulse() {
|
||||
var i;
|
||||
Search._pulse_status = (Search._pulse_status + 1) % 4;
|
||||
var dotString = '';
|
||||
for (i = 0; i < Search._pulse_status; i++)
|
||||
dotString += '.';
|
||||
Search.dots.text(dotString);
|
||||
if (Search._pulse_status > -1)
|
||||
window.setTimeout(pulse, 500);
|
||||
}
|
||||
pulse();
|
||||
},
|
||||
|
||||
/**
|
||||
* perform a search for something (or wait until index is loaded)
|
||||
*/
|
||||
performSearch : function(query) {
|
||||
// create the required interface elements
|
||||
this.out = $('#search-results');
|
||||
this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
|
||||
this.dots = $('<span></span>').appendTo(this.title);
|
||||
this.status = $('<p class="search-summary"> </p>').appendTo(this.out);
|
||||
this.output = $('<ul class="search"/>').appendTo(this.out);
|
||||
|
||||
$('#search-progress').text(_('Preparing search...'));
|
||||
this.startPulse();
|
||||
|
||||
// index already loaded, the browser was quick!
|
||||
if (this.hasIndex())
|
||||
this.query(query);
|
||||
else
|
||||
this.deferQuery(query);
|
||||
},
|
||||
|
||||
/**
|
||||
* execute search (requires search index to be loaded)
|
||||
*/
|
||||
query : function(query) {
|
||||
var i;
|
||||
|
||||
// stem the searchterms and add them to the correct list
|
||||
var stemmer = new Stemmer();
|
||||
var searchterms = [];
|
||||
var excluded = [];
|
||||
var hlterms = [];
|
||||
var tmp = splitQuery(query);
|
||||
var objectterms = [];
|
||||
for (i = 0; i < tmp.length; i++) {
|
||||
if (tmp[i] !== "") {
|
||||
objectterms.push(tmp[i].toLowerCase());
|
||||
}
|
||||
|
||||
if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
|
||||
// skip this "word"
|
||||
continue;
|
||||
}
|
||||
// stem the word
|
||||
var word = stemmer.stemWord(tmp[i].toLowerCase());
|
||||
// prevent stemmer from cutting word smaller than two chars
|
||||
if(word.length < 3 && tmp[i].length >= 3) {
|
||||
word = tmp[i];
|
||||
}
|
||||
var toAppend;
|
||||
// select the correct list
|
||||
if (word[0] == '-') {
|
||||
toAppend = excluded;
|
||||
word = word.substr(1);
|
||||
}
|
||||
else {
|
||||
toAppend = searchterms;
|
||||
hlterms.push(tmp[i].toLowerCase());
|
||||
}
|
||||
// only add if not already in the list
|
||||
if (!$u.contains(toAppend, word))
|
||||
toAppend.push(word);
|
||||
}
|
||||
var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
|
||||
|
||||
// console.debug('SEARCH: searching for:');
|
||||
// console.info('required: ', searchterms);
|
||||
// console.info('excluded: ', excluded);
|
||||
|
||||
// prepare search
|
||||
var terms = this._index.terms;
|
||||
var titleterms = this._index.titleterms;
|
||||
|
||||
// array of [filename, title, anchor, descr, score]
|
||||
var results = [];
|
||||
$('#search-progress').empty();
|
||||
|
||||
// lookup as object
|
||||
for (i = 0; i < objectterms.length; i++) {
|
||||
var others = [].concat(objectterms.slice(0, i),
|
||||
objectterms.slice(i+1, objectterms.length));
|
||||
results = results.concat(this.performObjectSearch(objectterms[i], others));
|
||||
}
|
||||
|
||||
// lookup as search terms in fulltext
|
||||
results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
|
||||
|
||||
// let the scorer override scores with a custom scoring function
|
||||
if (Scorer.score) {
|
||||
for (i = 0; i < results.length; i++)
|
||||
results[i][4] = Scorer.score(results[i]);
|
||||
}
|
||||
|
||||
// now sort the results by score (in opposite order of appearance, since the
|
||||
// display function below uses pop() to retrieve items) and then
|
||||
// alphabetically
|
||||
results.sort(function(a, b) {
|
||||
var left = a[4];
|
||||
var right = b[4];
|
||||
if (left > right) {
|
||||
return 1;
|
||||
} else if (left < right) {
|
||||
return -1;
|
||||
} else {
|
||||
// same score: sort alphabetically
|
||||
left = a[1].toLowerCase();
|
||||
right = b[1].toLowerCase();
|
||||
return (left > right) ? -1 : ((left < right) ? 1 : 0);
|
||||
}
|
||||
});
|
||||
|
||||
// for debugging
|
||||
//Search.lastresults = results.slice(); // a copy
|
||||
//console.info('search results:', Search.lastresults);
|
||||
|
||||
// print the results
|
||||
var resultCount = results.length;
|
||||
function displayNextItem() {
|
||||
// results left, load the summary and display it
|
||||
if (results.length) {
|
||||
var item = results.pop();
|
||||
var listItem = $('<li style="display:none"></li>');
|
||||
var requestUrl = "";
|
||||
var linkUrl = "";
|
||||
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
|
||||
// dirhtml builder
|
||||
var dirname = item[0] + '/';
|
||||
if (dirname.match(/\/index\/$/)) {
|
||||
dirname = dirname.substring(0, dirname.length-6);
|
||||
} else if (dirname == 'index/') {
|
||||
dirname = '';
|
||||
}
|
||||
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname;
|
||||
linkUrl = requestUrl;
|
||||
|
||||
} else {
|
||||
// normal html builders
|
||||
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX;
|
||||
linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX;
|
||||
}
|
||||
listItem.append($('<a/>').attr('href',
|
||||
linkUrl +
|
||||
highlightstring + item[2]).html(item[1]));
|
||||
if (item[3]) {
|
||||
listItem.append($('<span> (' + item[3] + ')</span>'));
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
displayNextItem();
|
||||
});
|
||||
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
|
||||
$.ajax({url: requestUrl,
|
||||
dataType: "text",
|
||||
complete: function(jqxhr, textstatus) {
|
||||
var data = jqxhr.responseText;
|
||||
if (data !== '' && data !== undefined) {
|
||||
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
|
||||
}
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
displayNextItem();
|
||||
});
|
||||
}});
|
||||
} else {
|
||||
// no source available, just display title
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
displayNextItem();
|
||||
});
|
||||
}
|
||||
}
|
||||
// search finished, update title and status message
|
||||
else {
|
||||
Search.stopPulse();
|
||||
Search.title.text(_('Search Results'));
|
||||
if (!resultCount)
|
||||
Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
|
||||
else
|
||||
Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
|
||||
Search.status.fadeIn(500);
|
||||
}
|
||||
}
|
||||
displayNextItem();
|
||||
},
|
||||
|
||||
/**
|
||||
* search for object names
|
||||
*/
|
||||
performObjectSearch : function(object, otherterms) {
|
||||
var filenames = this._index.filenames;
|
||||
var docnames = this._index.docnames;
|
||||
var objects = this._index.objects;
|
||||
var objnames = this._index.objnames;
|
||||
var titles = this._index.titles;
|
||||
|
||||
var i;
|
||||
var results = [];
|
||||
|
||||
for (var prefix in objects) {
|
||||
for (var name in objects[prefix]) {
|
||||
var fullname = (prefix ? prefix + '.' : '') + name;
|
||||
var fullnameLower = fullname.toLowerCase()
|
||||
if (fullnameLower.indexOf(object) > -1) {
|
||||
var score = 0;
|
||||
var parts = fullnameLower.split('.');
|
||||
// check for different match types: exact matches of full name or
|
||||
// "last name" (i.e. last dotted part)
|
||||
if (fullnameLower == object || parts[parts.length - 1] == object) {
|
||||
score += Scorer.objNameMatch;
|
||||
// matches in last name
|
||||
} else if (parts[parts.length - 1].indexOf(object) > -1) {
|
||||
score += Scorer.objPartialMatch;
|
||||
}
|
||||
var match = objects[prefix][name];
|
||||
var objname = objnames[match[1]][2];
|
||||
var title = titles[match[0]];
|
||||
// If more than one term searched for, we require other words to be
|
||||
// found in the name/title/description
|
||||
if (otherterms.length > 0) {
|
||||
var haystack = (prefix + ' ' + name + ' ' +
|
||||
objname + ' ' + title).toLowerCase();
|
||||
var allfound = true;
|
||||
for (i = 0; i < otherterms.length; i++) {
|
||||
if (haystack.indexOf(otherterms[i]) == -1) {
|
||||
allfound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allfound) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var descr = objname + _(', in ') + title;
|
||||
|
||||
var anchor = match[3];
|
||||
if (anchor === '')
|
||||
anchor = fullname;
|
||||
else if (anchor == '-')
|
||||
anchor = objnames[match[1]][1] + '-' + fullname;
|
||||
// add custom score for some objects according to scorer
|
||||
if (Scorer.objPrio.hasOwnProperty(match[2])) {
|
||||
score += Scorer.objPrio[match[2]];
|
||||
} else {
|
||||
score += Scorer.objPrioDefault;
|
||||
}
|
||||
results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* search for full-text terms in the index
|
||||
*/
|
||||
performTermsSearch : function(searchterms, excluded, terms, titleterms) {
|
||||
var docnames = this._index.docnames;
|
||||
var filenames = this._index.filenames;
|
||||
var titles = this._index.titles;
|
||||
|
||||
var i, j, file;
|
||||
var fileMap = {};
|
||||
var scoreMap = {};
|
||||
var results = [];
|
||||
|
||||
// perform the search on the required terms
|
||||
for (i = 0; i < searchterms.length; i++) {
|
||||
var word = searchterms[i];
|
||||
var files = [];
|
||||
var _o = [
|
||||
{files: terms[word], score: Scorer.term},
|
||||
{files: titleterms[word], score: Scorer.title}
|
||||
];
|
||||
// add support for partial matches
|
||||
if (word.length > 2) {
|
||||
for (var w in terms) {
|
||||
if (w.match(word) && !terms[word]) {
|
||||
_o.push({files: terms[w], score: Scorer.partialTerm})
|
||||
}
|
||||
}
|
||||
for (var w in titleterms) {
|
||||
if (w.match(word) && !titleterms[word]) {
|
||||
_o.push({files: titleterms[w], score: Scorer.partialTitle})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no match but word was a required one
|
||||
if ($u.every(_o, function(o){return o.files === undefined;})) {
|
||||
break;
|
||||
}
|
||||
// found search word in contents
|
||||
$u.each(_o, function(o) {
|
||||
var _files = o.files;
|
||||
if (_files === undefined)
|
||||
return
|
||||
|
||||
if (_files.length === undefined)
|
||||
_files = [_files];
|
||||
files = files.concat(_files);
|
||||
|
||||
// set score for the word in each file to Scorer.term
|
||||
for (j = 0; j < _files.length; j++) {
|
||||
file = _files[j];
|
||||
if (!(file in scoreMap))
|
||||
scoreMap[file] = {};
|
||||
scoreMap[file][word] = o.score;
|
||||
}
|
||||
});
|
||||
|
||||
// create the mapping
|
||||
for (j = 0; j < files.length; j++) {
|
||||
file = files[j];
|
||||
if (file in fileMap && fileMap[file].indexOf(word) === -1)
|
||||
fileMap[file].push(word);
|
||||
else
|
||||
fileMap[file] = [word];
|
||||
}
|
||||
}
|
||||
|
||||
// now check if the files don't contain excluded terms
|
||||
for (file in fileMap) {
|
||||
var valid = true;
|
||||
|
||||
// check if all requirements are matched
|
||||
var filteredTermCount = // as search terms with length < 3 are discarded: ignore
|
||||
searchterms.filter(function(term){return term.length > 2}).length
|
||||
if (
|
||||
fileMap[file].length != searchterms.length &&
|
||||
fileMap[file].length != filteredTermCount
|
||||
) continue;
|
||||
|
||||
// ensure that none of the excluded terms is in the search result
|
||||
for (i = 0; i < excluded.length; i++) {
|
||||
if (terms[excluded[i]] == file ||
|
||||
titleterms[excluded[i]] == file ||
|
||||
$u.contains(terms[excluded[i]] || [], file) ||
|
||||
$u.contains(titleterms[excluded[i]] || [], file)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have still a valid result we can add it to the result list
|
||||
if (valid) {
|
||||
// select one (max) score for the file.
|
||||
// for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
|
||||
var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
|
||||
results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* helper function to return a node containing the
|
||||
* search summary for a given text. keywords is a list
|
||||
* of stemmed words, hlwords is the list of normal, unstemmed
|
||||
* words. the first one is used to find the occurrence, the
|
||||
* latter for highlighting it.
|
||||
*/
|
||||
makeSearchSummary : function(htmlText, keywords, hlwords) {
|
||||
var text = Search.htmlToText(htmlText);
|
||||
var textLower = text.toLowerCase();
|
||||
var start = 0;
|
||||
$.each(keywords, function() {
|
||||
var i = textLower.indexOf(this.toLowerCase());
|
||||
if (i > -1)
|
||||
start = i;
|
||||
});
|
||||
start = Math.max(start - 120, 0);
|
||||
var excerpt = ((start > 0) ? '...' : '') +
|
||||
$.trim(text.substr(start, 240)) +
|
||||
((start + 240 - text.length) ? '...' : '');
|
||||
var rv = $('<div class="context"></div>').text(excerpt);
|
||||
$.each(hlwords, function() {
|
||||
rv = rv.highlightText(this, 'highlighted');
|
||||
});
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
Search.init();
|
||||
});
|
||||
999
docs/_static/underscore-1.3.1.js
vendored
Normal file
@@ -0,0 +1,999 @@
|
||||
// Underscore.js 1.3.1
|
||||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Underscore is freely distributable under the MIT license.
|
||||
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/underscore
|
||||
|
||||
(function() {
|
||||
|
||||
// Baseline setup
|
||||
// --------------
|
||||
|
||||
// Establish the root object, `window` in the browser, or `global` on the server.
|
||||
var root = this;
|
||||
|
||||
// Save the previous value of the `_` variable.
|
||||
var previousUnderscore = root._;
|
||||
|
||||
// Establish the object that gets returned to break out of a loop iteration.
|
||||
var breaker = {};
|
||||
|
||||
// Save bytes in the minified (but not gzipped) version:
|
||||
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
||||
|
||||
// Create quick reference variables for speed access to core prototypes.
|
||||
var slice = ArrayProto.slice,
|
||||
unshift = ArrayProto.unshift,
|
||||
toString = ObjProto.toString,
|
||||
hasOwnProperty = ObjProto.hasOwnProperty;
|
||||
|
||||
// All **ECMAScript 5** native function implementations that we hope to use
|
||||
// are declared here.
|
||||
var
|
||||
nativeForEach = ArrayProto.forEach,
|
||||
nativeMap = ArrayProto.map,
|
||||
nativeReduce = ArrayProto.reduce,
|
||||
nativeReduceRight = ArrayProto.reduceRight,
|
||||
nativeFilter = ArrayProto.filter,
|
||||
nativeEvery = ArrayProto.every,
|
||||
nativeSome = ArrayProto.some,
|
||||
nativeIndexOf = ArrayProto.indexOf,
|
||||
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
||||
nativeIsArray = Array.isArray,
|
||||
nativeKeys = Object.keys,
|
||||
nativeBind = FuncProto.bind;
|
||||
|
||||
// Create a safe reference to the Underscore object for use below.
|
||||
var _ = function(obj) { return new wrapper(obj); };
|
||||
|
||||
// Export the Underscore object for **Node.js**, with
|
||||
// backwards-compatibility for the old `require()` API. If we're in
|
||||
// the browser, add `_` as a global object via a string identifier,
|
||||
// for Closure Compiler "advanced" mode.
|
||||
if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
exports = module.exports = _;
|
||||
}
|
||||
exports._ = _;
|
||||
} else {
|
||||
root['_'] = _;
|
||||
}
|
||||
|
||||
// Current version.
|
||||
_.VERSION = '1.3.1';
|
||||
|
||||
// Collection Functions
|
||||
// --------------------
|
||||
|
||||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||||
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
||||
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
||||
var each = _.each = _.forEach = function(obj, iterator, context) {
|
||||
if (obj == null) return;
|
||||
if (nativeForEach && obj.forEach === nativeForEach) {
|
||||
obj.forEach(iterator, context);
|
||||
} else if (obj.length === +obj.length) {
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
|
||||
}
|
||||
} else {
|
||||
for (var key in obj) {
|
||||
if (_.has(obj, key)) {
|
||||
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return the results of applying the iterator to each element.
|
||||
// Delegates to **ECMAScript 5**'s native `map` if available.
|
||||
_.map = _.collect = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
results[results.length] = iterator.call(context, value, index, list);
|
||||
});
|
||||
if (obj.length === +obj.length) results.length = obj.length;
|
||||
return results;
|
||||
};
|
||||
|
||||
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
||||
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
||||
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduce && obj.reduce === nativeReduce) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
||||
}
|
||||
each(obj, function(value, index, list) {
|
||||
if (!initial) {
|
||||
memo = value;
|
||||
initial = true;
|
||||
} else {
|
||||
memo = iterator.call(context, memo, value, index, list);
|
||||
}
|
||||
});
|
||||
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
|
||||
return memo;
|
||||
};
|
||||
|
||||
// The right-associative version of reduce, also known as `foldr`.
|
||||
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
||||
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
||||
}
|
||||
var reversed = _.toArray(obj).reverse();
|
||||
if (context && !initial) iterator = _.bind(iterator, context);
|
||||
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
|
||||
};
|
||||
|
||||
// Return the first value which passes a truth test. Aliased as `detect`.
|
||||
_.find = _.detect = function(obj, iterator, context) {
|
||||
var result;
|
||||
any(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Return all the elements that pass a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
||||
// Aliased as `select`.
|
||||
_.filter = _.select = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Return all the elements for which a truth test fails.
|
||||
_.reject = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
each(obj, function(value, index, list) {
|
||||
if (!iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Determine whether all of the elements match a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `every` if available.
|
||||
// Aliased as `all`.
|
||||
_.every = _.all = function(obj, iterator, context) {
|
||||
var result = true;
|
||||
if (obj == null) return result;
|
||||
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Determine if at least one element in the object matches a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `some` if available.
|
||||
// Aliased as `any`.
|
||||
var any = _.some = _.any = function(obj, iterator, context) {
|
||||
iterator || (iterator = _.identity);
|
||||
var result = false;
|
||||
if (obj == null) return result;
|
||||
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return !!result;
|
||||
};
|
||||
|
||||
// Determine if a given value is included in the array or object using `===`.
|
||||
// Aliased as `contains`.
|
||||
_.include = _.contains = function(obj, target) {
|
||||
var found = false;
|
||||
if (obj == null) return found;
|
||||
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
||||
found = any(obj, function(value) {
|
||||
return value === target;
|
||||
});
|
||||
return found;
|
||||
};
|
||||
|
||||
// Invoke a method (with arguments) on every item in a collection.
|
||||
_.invoke = function(obj, method) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return _.map(obj, function(value) {
|
||||
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
|
||||
});
|
||||
};
|
||||
|
||||
// Convenience version of a common use case of `map`: fetching a property.
|
||||
_.pluck = function(obj, key) {
|
||||
return _.map(obj, function(value){ return value[key]; });
|
||||
};
|
||||
|
||||
// Return the maximum element or (element-based computation).
|
||||
_.max = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
||||
var result = {computed : -Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed >= result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Return the minimum element (or element-based computation).
|
||||
_.min = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return Infinity;
|
||||
var result = {computed : Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed < result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Shuffle an array.
|
||||
_.shuffle = function(obj) {
|
||||
var shuffled = [], rand;
|
||||
each(obj, function(value, index, list) {
|
||||
if (index == 0) {
|
||||
shuffled[0] = value;
|
||||
} else {
|
||||
rand = Math.floor(Math.random() * (index + 1));
|
||||
shuffled[index] = shuffled[rand];
|
||||
shuffled[rand] = value;
|
||||
}
|
||||
});
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
// Sort the object's values by a criterion produced by an iterator.
|
||||
_.sortBy = function(obj, iterator, context) {
|
||||
return _.pluck(_.map(obj, function(value, index, list) {
|
||||
return {
|
||||
value : value,
|
||||
criteria : iterator.call(context, value, index, list)
|
||||
};
|
||||
}).sort(function(left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}), 'value');
|
||||
};
|
||||
|
||||
// Groups the object's values by a criterion. Pass either a string attribute
|
||||
// to group by, or a function that returns the criterion.
|
||||
_.groupBy = function(obj, val) {
|
||||
var result = {};
|
||||
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
|
||||
each(obj, function(value, index) {
|
||||
var key = iterator(value, index);
|
||||
(result[key] || (result[key] = [])).push(value);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Use a comparator function to figure out at what index an object should
|
||||
// be inserted so as to maintain order. Uses binary search.
|
||||
_.sortedIndex = function(array, obj, iterator) {
|
||||
iterator || (iterator = _.identity);
|
||||
var low = 0, high = array.length;
|
||||
while (low < high) {
|
||||
var mid = (low + high) >> 1;
|
||||
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
|
||||
}
|
||||
return low;
|
||||
};
|
||||
|
||||
// Safely convert anything iterable into a real, live array.
|
||||
_.toArray = function(iterable) {
|
||||
if (!iterable) return [];
|
||||
if (iterable.toArray) return iterable.toArray();
|
||||
if (_.isArray(iterable)) return slice.call(iterable);
|
||||
if (_.isArguments(iterable)) return slice.call(iterable);
|
||||
return _.values(iterable);
|
||||
};
|
||||
|
||||
// Return the number of elements in an object.
|
||||
_.size = function(obj) {
|
||||
return _.toArray(obj).length;
|
||||
};
|
||||
|
||||
// Array Functions
|
||||
// ---------------
|
||||
|
||||
// Get the first element of an array. Passing **n** will return the first N
|
||||
// values in the array. Aliased as `head`. The **guard** check allows it to work
|
||||
// with `_.map`.
|
||||
_.first = _.head = function(array, n, guard) {
|
||||
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
||||
};
|
||||
|
||||
// Returns everything but the last entry of the array. Especcialy useful on
|
||||
// the arguments object. Passing **n** will return all the values in
|
||||
// the array, excluding the last N. The **guard** check allows it to work with
|
||||
// `_.map`.
|
||||
_.initial = function(array, n, guard) {
|
||||
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
||||
};
|
||||
|
||||
// Get the last element of an array. Passing **n** will return the last N
|
||||
// values in the array. The **guard** check allows it to work with `_.map`.
|
||||
_.last = function(array, n, guard) {
|
||||
if ((n != null) && !guard) {
|
||||
return slice.call(array, Math.max(array.length - n, 0));
|
||||
} else {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
// Returns everything but the first entry of the array. Aliased as `tail`.
|
||||
// Especially useful on the arguments object. Passing an **index** will return
|
||||
// the rest of the values in the array from that index onward. The **guard**
|
||||
// check allows it to work with `_.map`.
|
||||
_.rest = _.tail = function(array, index, guard) {
|
||||
return slice.call(array, (index == null) || guard ? 1 : index);
|
||||
};
|
||||
|
||||
// Trim out all falsy values from an array.
|
||||
_.compact = function(array) {
|
||||
return _.filter(array, function(value){ return !!value; });
|
||||
};
|
||||
|
||||
// Return a completely flattened version of an array.
|
||||
_.flatten = function(array, shallow) {
|
||||
return _.reduce(array, function(memo, value) {
|
||||
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
|
||||
memo[memo.length] = value;
|
||||
return memo;
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Return a version of the array that does not contain the specified value(s).
|
||||
_.without = function(array) {
|
||||
return _.difference(array, slice.call(arguments, 1));
|
||||
};
|
||||
|
||||
// Produce a duplicate-free version of the array. If the array has already
|
||||
// been sorted, you have the option of using a faster algorithm.
|
||||
// Aliased as `unique`.
|
||||
_.uniq = _.unique = function(array, isSorted, iterator) {
|
||||
var initial = iterator ? _.map(array, iterator) : array;
|
||||
var result = [];
|
||||
_.reduce(initial, function(memo, el, i) {
|
||||
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
|
||||
memo[memo.length] = el;
|
||||
result[result.length] = array[i];
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Produce an array that contains the union: each distinct element from all of
|
||||
// the passed-in arrays.
|
||||
_.union = function() {
|
||||
return _.uniq(_.flatten(arguments, true));
|
||||
};
|
||||
|
||||
// Produce an array that contains every item shared between all the
|
||||
// passed-in arrays. (Aliased as "intersect" for back-compat.)
|
||||
_.intersection = _.intersect = function(array) {
|
||||
var rest = slice.call(arguments, 1);
|
||||
return _.filter(_.uniq(array), function(item) {
|
||||
return _.every(rest, function(other) {
|
||||
return _.indexOf(other, item) >= 0;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Take the difference between one array and a number of other arrays.
|
||||
// Only the elements present in just the first array will remain.
|
||||
_.difference = function(array) {
|
||||
var rest = _.flatten(slice.call(arguments, 1));
|
||||
return _.filter(array, function(value){ return !_.include(rest, value); });
|
||||
};
|
||||
|
||||
// Zip together multiple lists into a single array -- elements that share
|
||||
// an index go together.
|
||||
_.zip = function() {
|
||||
var args = slice.call(arguments);
|
||||
var length = _.max(_.pluck(args, 'length'));
|
||||
var results = new Array(length);
|
||||
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
|
||||
return results;
|
||||
};
|
||||
|
||||
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
||||
// we need this function. Return the position of the first occurrence of an
|
||||
// item in an array, or -1 if the item is not included in the array.
|
||||
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
||||
// If the array is large and already in sort order, pass `true`
|
||||
// for **isSorted** to use binary search.
|
||||
_.indexOf = function(array, item, isSorted) {
|
||||
if (array == null) return -1;
|
||||
var i, l;
|
||||
if (isSorted) {
|
||||
i = _.sortedIndex(array, item);
|
||||
return array[i] === item ? i : -1;
|
||||
}
|
||||
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
|
||||
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
||||
_.lastIndexOf = function(array, item) {
|
||||
if (array == null) return -1;
|
||||
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
|
||||
var i = array.length;
|
||||
while (i--) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Generate an integer Array containing an arithmetic progression. A port of
|
||||
// the native Python `range()` function. See
|
||||
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
||||
_.range = function(start, stop, step) {
|
||||
if (arguments.length <= 1) {
|
||||
stop = start || 0;
|
||||
start = 0;
|
||||
}
|
||||
step = arguments[2] || 1;
|
||||
|
||||
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
||||
var idx = 0;
|
||||
var range = new Array(len);
|
||||
|
||||
while(idx < len) {
|
||||
range[idx++] = start;
|
||||
start += step;
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
// Function (ahem) Functions
|
||||
// ------------------
|
||||
|
||||
// Reusable constructor function for prototype setting.
|
||||
var ctor = function(){};
|
||||
|
||||
// Create a function bound to a given object (assigning `this`, and arguments,
|
||||
// optionally). Binding with arguments is also known as `curry`.
|
||||
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
||||
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
||||
_.bind = function bind(func, context) {
|
||||
var bound, args;
|
||||
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
||||
if (!_.isFunction(func)) throw new TypeError;
|
||||
args = slice.call(arguments, 2);
|
||||
return bound = function() {
|
||||
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
||||
ctor.prototype = func.prototype;
|
||||
var self = new ctor;
|
||||
var result = func.apply(self, args.concat(slice.call(arguments)));
|
||||
if (Object(result) === result) return result;
|
||||
return self;
|
||||
};
|
||||
};
|
||||
|
||||
// Bind all of an object's methods to that object. Useful for ensuring that
|
||||
// all callbacks defined on an object belong to it.
|
||||
_.bindAll = function(obj) {
|
||||
var funcs = slice.call(arguments, 1);
|
||||
if (funcs.length == 0) funcs = _.functions(obj);
|
||||
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Memoize an expensive function by storing its results.
|
||||
_.memoize = function(func, hasher) {
|
||||
var memo = {};
|
||||
hasher || (hasher = _.identity);
|
||||
return function() {
|
||||
var key = hasher.apply(this, arguments);
|
||||
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
||||
};
|
||||
};
|
||||
|
||||
// Delays a function for the given number of milliseconds, and then calls
|
||||
// it with the arguments supplied.
|
||||
_.delay = function(func, wait) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return setTimeout(function(){ return func.apply(func, args); }, wait);
|
||||
};
|
||||
|
||||
// Defers a function, scheduling it to run after the current call stack has
|
||||
// cleared.
|
||||
_.defer = function(func) {
|
||||
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
||||
};
|
||||
|
||||
// Returns a function, that, when invoked, will only be triggered at most once
|
||||
// during a given window of time.
|
||||
_.throttle = function(func, wait) {
|
||||
var context, args, timeout, throttling, more;
|
||||
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
|
||||
return function() {
|
||||
context = this; args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (more) func.apply(context, args);
|
||||
whenDone();
|
||||
};
|
||||
if (!timeout) timeout = setTimeout(later, wait);
|
||||
if (throttling) {
|
||||
more = true;
|
||||
} else {
|
||||
func.apply(context, args);
|
||||
}
|
||||
whenDone();
|
||||
throttling = true;
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds.
|
||||
_.debounce = function(func, wait) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will be executed at most one time, no matter how
|
||||
// often you call it. Useful for lazy initialization.
|
||||
_.once = function(func) {
|
||||
var ran = false, memo;
|
||||
return function() {
|
||||
if (ran) return memo;
|
||||
ran = true;
|
||||
return memo = func.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns the first function passed as an argument to the second,
|
||||
// allowing you to adjust arguments, run code before and after, and
|
||||
// conditionally execute the original function.
|
||||
_.wrap = function(func, wrapper) {
|
||||
return function() {
|
||||
var args = [func].concat(slice.call(arguments, 0));
|
||||
return wrapper.apply(this, args);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that is the composition of a list of functions, each
|
||||
// consuming the return value of the function that follows.
|
||||
_.compose = function() {
|
||||
var funcs = arguments;
|
||||
return function() {
|
||||
var args = arguments;
|
||||
for (var i = funcs.length - 1; i >= 0; i--) {
|
||||
args = [funcs[i].apply(this, args)];
|
||||
}
|
||||
return args[0];
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will only be executed after being called N times.
|
||||
_.after = function(times, func) {
|
||||
if (times <= 0) return func();
|
||||
return function() {
|
||||
if (--times < 1) { return func.apply(this, arguments); }
|
||||
};
|
||||
};
|
||||
|
||||
// Object Functions
|
||||
// ----------------
|
||||
|
||||
// Retrieve the names of an object's properties.
|
||||
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
||||
_.keys = nativeKeys || function(obj) {
|
||||
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
||||
var keys = [];
|
||||
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Retrieve the values of an object's properties.
|
||||
_.values = function(obj) {
|
||||
return _.map(obj, _.identity);
|
||||
};
|
||||
|
||||
// Return a sorted list of the function names available on the object.
|
||||
// Aliased as `methods`
|
||||
_.functions = _.methods = function(obj) {
|
||||
var names = [];
|
||||
for (var key in obj) {
|
||||
if (_.isFunction(obj[key])) names.push(key);
|
||||
}
|
||||
return names.sort();
|
||||
};
|
||||
|
||||
// Extend a given object with all the properties in passed-in object(s).
|
||||
_.extend = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Fill in a given object with default properties.
|
||||
_.defaults = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
if (obj[prop] == null) obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Create a (shallow-cloned) duplicate of an object.
|
||||
_.clone = function(obj) {
|
||||
if (!_.isObject(obj)) return obj;
|
||||
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
||||
};
|
||||
|
||||
// Invokes interceptor with the obj, and then returns obj.
|
||||
// The primary purpose of this method is to "tap into" a method chain, in
|
||||
// order to perform operations on intermediate results within the chain.
|
||||
_.tap = function(obj, interceptor) {
|
||||
interceptor(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Internal recursive comparison function.
|
||||
function eq(a, b, stack) {
|
||||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||||
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
||||
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
||||
// A strict comparison is necessary because `null == undefined`.
|
||||
if (a == null || b == null) return a === b;
|
||||
// Unwrap any wrapped objects.
|
||||
if (a._chain) a = a._wrapped;
|
||||
if (b._chain) b = b._wrapped;
|
||||
// Invoke a custom `isEqual` method if one is provided.
|
||||
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
|
||||
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
|
||||
// Compare `[[Class]]` names.
|
||||
var className = toString.call(a);
|
||||
if (className != toString.call(b)) return false;
|
||||
switch (className) {
|
||||
// Strings, numbers, dates, and booleans are compared by value.
|
||||
case '[object String]':
|
||||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||||
// equivalent to `new String("5")`.
|
||||
return a == String(b);
|
||||
case '[object Number]':
|
||||
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
||||
// other numeric values.
|
||||
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
||||
case '[object Date]':
|
||||
case '[object Boolean]':
|
||||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||||
// millisecond representations. Note that invalid dates with millisecond representations
|
||||
// of `NaN` are not equivalent.
|
||||
return +a == +b;
|
||||
// RegExps are compared by their source patterns and flags.
|
||||
case '[object RegExp]':
|
||||
return a.source == b.source &&
|
||||
a.global == b.global &&
|
||||
a.multiline == b.multiline &&
|
||||
a.ignoreCase == b.ignoreCase;
|
||||
}
|
||||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||||
var length = stack.length;
|
||||
while (length--) {
|
||||
// Linear search. Performance is inversely proportional to the number of
|
||||
// unique nested structures.
|
||||
if (stack[length] == a) return true;
|
||||
}
|
||||
// Add the first object to the stack of traversed objects.
|
||||
stack.push(a);
|
||||
var size = 0, result = true;
|
||||
// Recursively compare objects and arrays.
|
||||
if (className == '[object Array]') {
|
||||
// Compare array lengths to determine if a deep comparison is necessary.
|
||||
size = a.length;
|
||||
result = size == b.length;
|
||||
if (result) {
|
||||
// Deep compare the contents, ignoring non-numeric properties.
|
||||
while (size--) {
|
||||
// Ensure commutative equality for sparse arrays.
|
||||
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Objects with different constructors are not equivalent.
|
||||
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
|
||||
// Deep compare objects.
|
||||
for (var key in a) {
|
||||
if (_.has(a, key)) {
|
||||
// Count the expected number of properties.
|
||||
size++;
|
||||
// Deep compare each member.
|
||||
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
|
||||
}
|
||||
}
|
||||
// Ensure that both objects contain the same number of properties.
|
||||
if (result) {
|
||||
for (key in b) {
|
||||
if (_.has(b, key) && !(size--)) break;
|
||||
}
|
||||
result = !size;
|
||||
}
|
||||
}
|
||||
// Remove the first object from the stack of traversed objects.
|
||||
stack.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Perform a deep comparison to check if two objects are equal.
|
||||
_.isEqual = function(a, b) {
|
||||
return eq(a, b, []);
|
||||
};
|
||||
|
||||
// Is a given array, string, or object empty?
|
||||
// An "empty" object has no enumerable own-properties.
|
||||
_.isEmpty = function(obj) {
|
||||
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
||||
for (var key in obj) if (_.has(obj, key)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Is a given value a DOM element?
|
||||
_.isElement = function(obj) {
|
||||
return !!(obj && obj.nodeType == 1);
|
||||
};
|
||||
|
||||
// Is a given value an array?
|
||||
// Delegates to ECMA5's native Array.isArray
|
||||
_.isArray = nativeIsArray || function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
|
||||
// Is a given variable an object?
|
||||
_.isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
|
||||
// Is a given variable an arguments object?
|
||||
_.isArguments = function(obj) {
|
||||
return toString.call(obj) == '[object Arguments]';
|
||||
};
|
||||
if (!_.isArguments(arguments)) {
|
||||
_.isArguments = function(obj) {
|
||||
return !!(obj && _.has(obj, 'callee'));
|
||||
};
|
||||
}
|
||||
|
||||
// Is a given value a function?
|
||||
_.isFunction = function(obj) {
|
||||
return toString.call(obj) == '[object Function]';
|
||||
};
|
||||
|
||||
// Is a given value a string?
|
||||
_.isString = function(obj) {
|
||||
return toString.call(obj) == '[object String]';
|
||||
};
|
||||
|
||||
// Is a given value a number?
|
||||
_.isNumber = function(obj) {
|
||||
return toString.call(obj) == '[object Number]';
|
||||
};
|
||||
|
||||
// Is the given value `NaN`?
|
||||
_.isNaN = function(obj) {
|
||||
// `NaN` is the only value for which `===` is not reflexive.
|
||||
return obj !== obj;
|
||||
};
|
||||
|
||||
// Is a given value a boolean?
|
||||
_.isBoolean = function(obj) {
|
||||
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
// Is a given value a date?
|
||||
_.isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
|
||||
// Is the given value a regular expression?
|
||||
_.isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
|
||||
// Is a given value equal to null?
|
||||
_.isNull = function(obj) {
|
||||
return obj === null;
|
||||
};
|
||||
|
||||
// Is a given variable undefined?
|
||||
_.isUndefined = function(obj) {
|
||||
return obj === void 0;
|
||||
};
|
||||
|
||||
// Has own property?
|
||||
_.has = function(obj, key) {
|
||||
return hasOwnProperty.call(obj, key);
|
||||
};
|
||||
|
||||
// Utility Functions
|
||||
// -----------------
|
||||
|
||||
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
||||
// previous owner. Returns a reference to the Underscore object.
|
||||
_.noConflict = function() {
|
||||
root._ = previousUnderscore;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Keep the identity function around for default iterators.
|
||||
_.identity = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
// Run a function **n** times.
|
||||
_.times = function (n, iterator, context) {
|
||||
for (var i = 0; i < n; i++) iterator.call(context, i);
|
||||
};
|
||||
|
||||
// Escape a string for HTML interpolation.
|
||||
_.escape = function(string) {
|
||||
return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
||||
};
|
||||
|
||||
// Add your own custom functions to the Underscore object, ensuring that
|
||||
// they're correctly added to the OOP wrapper as well.
|
||||
_.mixin = function(obj) {
|
||||
each(_.functions(obj), function(name){
|
||||
addToWrapper(name, _[name] = obj[name]);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate a unique integer id (unique within the entire client session).
|
||||
// Useful for temporary DOM ids.
|
||||
var idCounter = 0;
|
||||
_.uniqueId = function(prefix) {
|
||||
var id = idCounter++;
|
||||
return prefix ? prefix + id : id;
|
||||
};
|
||||
|
||||
// By default, Underscore uses ERB-style template delimiters, change the
|
||||
// following template settings to use alternative delimiters.
|
||||
_.templateSettings = {
|
||||
evaluate : /<%([\s\S]+?)%>/g,
|
||||
interpolate : /<%=([\s\S]+?)%>/g,
|
||||
escape : /<%-([\s\S]+?)%>/g
|
||||
};
|
||||
|
||||
// When customizing `templateSettings`, if you don't want to define an
|
||||
// interpolation, evaluation or escaping regex, we need one that is
|
||||
// guaranteed not to match.
|
||||
var noMatch = /.^/;
|
||||
|
||||
// Within an interpolation, evaluation, or escaping, remove HTML escaping
|
||||
// that had been previously added.
|
||||
var unescape = function(code) {
|
||||
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
|
||||
};
|
||||
|
||||
// JavaScript micro-templating, similar to John Resig's implementation.
|
||||
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
||||
// and correctly escapes quotes within interpolated code.
|
||||
_.template = function(str, data) {
|
||||
var c = _.templateSettings;
|
||||
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
|
||||
'with(obj||{}){__p.push(\'' +
|
||||
str.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(c.escape || noMatch, function(match, code) {
|
||||
return "',_.escape(" + unescape(code) + "),'";
|
||||
})
|
||||
.replace(c.interpolate || noMatch, function(match, code) {
|
||||
return "'," + unescape(code) + ",'";
|
||||
})
|
||||
.replace(c.evaluate || noMatch, function(match, code) {
|
||||
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
|
||||
})
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\t/g, '\\t')
|
||||
+ "');}return __p.join('');";
|
||||
var func = new Function('obj', '_', tmpl);
|
||||
if (data) return func(data, _);
|
||||
return function(data) {
|
||||
return func.call(this, data, _);
|
||||
};
|
||||
};
|
||||
|
||||
// Add a "chain" function, which will delegate to the wrapper.
|
||||
_.chain = function(obj) {
|
||||
return _(obj).chain();
|
||||
};
|
||||
|
||||
// The OOP Wrapper
|
||||
// ---------------
|
||||
|
||||
// If Underscore is called as a function, it returns a wrapped object that
|
||||
// can be used OO-style. This wrapper holds altered versions of all the
|
||||
// underscore functions. Wrapped objects may be chained.
|
||||
var wrapper = function(obj) { this._wrapped = obj; };
|
||||
|
||||
// Expose `wrapper.prototype` as `_.prototype`
|
||||
_.prototype = wrapper.prototype;
|
||||
|
||||
// Helper function to continue chaining intermediate results.
|
||||
var result = function(obj, chain) {
|
||||
return chain ? _(obj).chain() : obj;
|
||||
};
|
||||
|
||||
// A method to easily add functions to the OOP wrapper.
|
||||
var addToWrapper = function(name, func) {
|
||||
wrapper.prototype[name] = function() {
|
||||
var args = slice.call(arguments);
|
||||
unshift.call(args, this._wrapped);
|
||||
return result(func.apply(_, args), this._chain);
|
||||
};
|
||||
};
|
||||
|
||||
// Add all of the Underscore functions to the wrapper object.
|
||||
_.mixin(_);
|
||||
|
||||
// Add all mutator Array functions to the wrapper.
|
||||
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
var wrapped = this._wrapped;
|
||||
method.apply(wrapped, arguments);
|
||||
var length = wrapped.length;
|
||||
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
|
||||
return result(wrapped, this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Add all accessor Array functions to the wrapper.
|
||||
each(['concat', 'join', 'slice'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
return result(method.apply(this._wrapped, arguments), this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Start chaining a wrapped Underscore object.
|
||||
wrapper.prototype.chain = function() {
|
||||
this._chain = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Extracts the result from a wrapped and chained object.
|
||||
wrapper.prototype.value = function() {
|
||||
return this._wrapped;
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
31
docs/_static/underscore.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
// Underscore.js 1.3.1
|
||||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Underscore is freely distributable under the MIT license.
|
||||
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/underscore
|
||||
(function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
|
||||
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,
|
||||
h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each=
|
||||
b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===n)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===n)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(x&&a.map===x)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==
|
||||
null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=
|
||||
function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e=
|
||||
e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
|
||||
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&(e={value:a,computed:b})});
|
||||
return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){f==0?b[0]=a:(d=Math.floor(Math.random()*(f+1)),b[f]=b[d],b[d]=a)});return b};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,g){return{value:a,criteria:c.call(d,a,b,g)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,
|
||||
c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:a.toArray?a.toArray():b.isArray(a)?i.call(a):b.isArguments(a)?i.call(a):b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=
|
||||
b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,e=[];b.reduce(d,function(d,g,h){if(0==h||(c===true?b.last(d)!=g:!b.include(d,g)))d[d.length]=g,e[e.length]=a[h];return d},[]);
|
||||
return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,
|
||||
d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(p&&a.indexOf===p)return a.indexOf(c);for(d=0,e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(D&&a.lastIndexOf===D)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};
|
||||
var F=function(){};b.bind=function(a,c){var d,e;if(a.bind===s&&s)return s.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));F.prototype=a.prototype;var b=new F,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,
|
||||
c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i=b.debounce(function(){h=g=false},c);return function(){d=this;e=arguments;var b;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);i()},c));g?h=true:
|
||||
a.apply(d,e);i();g=true}};b.debounce=function(a,b){var d;return function(){var e=this,f=arguments;clearTimeout(d);d=setTimeout(function(){d=null;a.apply(e,f)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};
|
||||
b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments,
|
||||
1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};
|
||||
b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};
|
||||
b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a),
|
||||
function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+
|
||||
u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]=
|
||||
function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=
|
||||
true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
|
||||
1441
docs/cli.html
Normal file
2120
docs/genindex.html
Normal file
380
docs/index.html
Normal file
@@ -0,0 +1,380 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="./" 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="next" title="osxphotos command line interface (CLI)" href="cli.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="welcome-to-osxphotos-s-documentation">
|
||||
<h1>Welcome to osxphotos’s documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline">¶</a></h1>
|
||||
</div>
|
||||
<div class="section" id="osxphotos">
|
||||
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="section" id="what-is-osxphotos">
|
||||
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline">¶</a></h2>
|
||||
<p>OSXPhotos provides both the ability to interact with and query Apple’s Photos.app library on macOS directly from your python code
|
||||
as well as a very flexible command line interface (CLI) app for exporting photos.
|
||||
You can query the Photos library database – for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
|
||||
You can also easily export both the original and edited photos.</p>
|
||||
</div>
|
||||
<div class="section" id="supported-operating-systems">
|
||||
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
|
||||
Beta support for macOS Big Sur (10.16.01/11.01).</p>
|
||||
<p>This package will read Photos databases for any supported version on any supported macOS version.
|
||||
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
|
||||
<p>Requires python >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline">¶</a></h2>
|
||||
<p>If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.</p>
|
||||
<div class="section" id="installation-using-pipx">
|
||||
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline">¶</a></h3>
|
||||
<p>If you aren’t familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Open <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> (search for <code class="docutils literal notranslate"><span class="pre">Terminal</span></code> in Spotlight or look in <code class="docutils literal notranslate"><span class="pre">Applications/Utilities</span></code>)</p></li>
|
||||
<li><p>Install <code class="docutils literal notranslate"><span class="pre">homebrew</span></code> according to instructions at <a class="reference external" href="https://brew.sh/">https://brew.sh/</a></p></li>
|
||||
<li><p>Type the following into Terminal: <code class="docutils literal notranslate"><span class="pre">brew</span> <span class="pre">install</span> <span class="pre">pipx</span></code></p></li>
|
||||
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
|
||||
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation-using-pip">
|
||||
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline">¶</a></h3>
|
||||
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="installation-from-git-repository">
|
||||
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline">¶</a></h3>
|
||||
<p>OSXPhotos uses setuptools, thus simply run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
|
||||
<span class="n">cd</span> <span class="n">osxphotos</span>
|
||||
<span class="n">python3</span> <span class="n">setup</span><span class="o">.</span><span class="n">py</span> <span class="n">install</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>I recommend you create a <a class="reference external" href="https://docs.python.org/3/tutorial/venv.html">virtual environment</a> before installing osxphotos.</p>
|
||||
<p><strong>WARNING</strong> The git repo for this project is very large (> 1GB) because it contains multiple Photos libraries used for testing
|
||||
on different versions of macOS. If you just want to use the osxphotos package in your own code,
|
||||
I recommend you install the latest version from <a class="reference external" href="https://pypi.org/project/osxphotos/">PyPI</a> which does not include all the test
|
||||
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
|
||||
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
|
||||
If you aren’t comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="command-line-usage">
|
||||
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h2>
|
||||
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
|
||||
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">></span> <span class="n">osxphotos</span>
|
||||
<span class="n">Usage</span><span class="p">:</span> <span class="n">osxphotos</span> <span class="p">[</span><span class="n">OPTIONS</span><span class="p">]</span> <span class="n">COMMAND</span> <span class="p">[</span><span class="n">ARGS</span><span class="p">]</span><span class="o">...</span>
|
||||
|
||||
<span class="n">Options</span><span class="p">:</span>
|
||||
<span class="o">--</span><span class="n">db</span> <span class="o"><</span><span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">></span> <span class="n">Specify</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">path</span><span class="o">.</span> <span class="n">Path</span> <span class="n">to</span> <span class="n">Photos</span>
|
||||
<span class="n">library</span><span class="o">/</span><span class="n">database</span> <span class="n">can</span> <span class="n">be</span> <span class="n">specified</span> <span class="n">using</span> <span class="n">either</span>
|
||||
<span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">directly</span> <span class="k">as</span> <span class="n">PHOTOS_LIBRARY</span> <span class="n">positional</span>
|
||||
<span class="n">argument</span><span class="o">.</span> <span class="n">If</span> <span class="n">neither</span> <span class="o">--</span><span class="n">db</span> <span class="ow">or</span> <span class="n">PHOTOS_LIBRARY</span>
|
||||
<span class="n">provided</span><span class="p">,</span> <span class="n">will</span> <span class="n">attempt</span> <span class="n">to</span> <span class="n">find</span> <span class="n">the</span> <span class="n">library</span> <span class="n">to</span>
|
||||
<span class="n">use</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">following</span> <span class="n">order</span><span class="p">:</span> <span class="mf">1.</span> <span class="n">last</span> <span class="n">opened</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="mf">2.</span> <span class="n">system</span> <span class="n">library</span><span class="p">,</span> <span class="mf">3.</span>
|
||||
<span class="o">~/</span><span class="n">Pictures</span><span class="o">/</span><span class="n">Photos</span> <span class="n">Library</span><span class="o">.</span><span class="n">photoslibrary</span>
|
||||
<span class="o">--</span><span class="n">json</span> <span class="n">Print</span> <span class="n">output</span> <span class="ow">in</span> <span class="n">JSON</span> <span class="nb">format</span><span class="o">.</span>
|
||||
<span class="o">-</span><span class="n">v</span><span class="p">,</span> <span class="o">--</span><span class="n">version</span> <span class="n">Show</span> <span class="n">the</span> <span class="n">version</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
|
||||
<span class="o">-</span><span class="n">h</span><span class="p">,</span> <span class="o">--</span><span class="n">help</span> <span class="n">Show</span> <span class="n">this</span> <span class="n">message</span> <span class="ow">and</span> <span class="n">exit</span><span class="o">.</span>
|
||||
|
||||
<span class="n">Commands</span><span class="p">:</span>
|
||||
<span class="n">about</span> <span class="n">Print</span> <span class="n">information</span> <span class="n">about</span> <span class="n">osxphotos</span> <span class="n">including</span> <span class="n">license</span><span class="o">.</span>
|
||||
<span class="n">albums</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">albums</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
|
||||
<span class="n">dump</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="nb">all</span> <span class="n">photos</span> <span class="o">&</span> <span class="n">associated</span> <span class="n">info</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span><span class="o">...</span>
|
||||
<span class="n">export</span> <span class="n">Export</span> <span class="n">photos</span> <span class="kn">from</span> <span class="nn">the</span> <span class="n">Photos</span> <span class="n">database</span><span class="o">.</span>
|
||||
<span class="n">help</span> <span class="n">Print</span> <span class="n">help</span><span class="p">;</span> <span class="k">for</span> <span class="n">help</span> <span class="n">on</span> <span class="n">commands</span><span class="p">:</span> <span class="n">help</span> <span class="o"><</span><span class="n">command</span><span class="o">>.</span>
|
||||
<span class="n">info</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">descriptive</span> <span class="n">info</span> <span class="n">of</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span> <span class="n">database</span><span class="o">.</span>
|
||||
<span class="n">keywords</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">keywords</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
|
||||
<span class="n">labels</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">image</span> <span class="n">classification</span> <span class="n">labels</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span><span class="o">...</span>
|
||||
<span class="nb">list</span> <span class="n">Print</span> <span class="nb">list</span> <span class="n">of</span> <span class="n">Photos</span> <span class="n">libraries</span> <span class="n">found</span> <span class="n">on</span> <span class="n">the</span> <span class="n">system</span><span class="o">.</span>
|
||||
<span class="n">persons</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">persons</span> <span class="p">(</span><span class="n">faces</span><span class="p">)</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
|
||||
<span class="n">places</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">places</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
|
||||
<span class="n">query</span> <span class="n">Query</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">using</span> <span class="mi">1</span> <span class="ow">or</span> <span class="n">more</span> <span class="n">search</span> <span class="n">options</span><span class="p">;</span> <span class="k">if</span><span class="o">...</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre"><command_name></span></code></p>
|
||||
<div class="section" id="command-line-examples">
|
||||
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline">¶</a></h3>
|
||||
<div class="section" id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
|
||||
<h4>export all photos to ~/Desktop/export group in folders by date created<a class="headerlink" href="#export-all-photos-to-desktop-export-group-in-folders-by-date-created" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
<p><strong>Note</strong>: Photos library/database path can also be specified using <code class="docutils literal notranslate"><span class="pre">--db</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">--db</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">>results.json</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
<h4>export photos to file structure based on 4-digit year and full name of month of photo’s creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{created.month}"</span></code></p>
|
||||
<p>(by default, it will attempt to use the system library)</p>
|
||||
</div>
|
||||
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
<h4>export photos to file structure based on 4-digit year of photo’s creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{media_type}"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
<h4>export default library using ‘country name/year’ as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="example-uses-of-the-package">
|
||||
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Simple usage of the package """</span>
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
|
||||
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons</span><span class="p">)</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">album_names</span><span class="p">)</span>
|
||||
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">keywords_as_dict</span><span class="p">)</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">persons_as_dict</span><span class="p">)</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="n">photosdb</span><span class="o">.</span><span class="n">albums_as_dict</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># find all photos with Keyword = Foo and containing John Smith</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">"Foo"</span><span class="p">],</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">"John Smith"</span><span class="p">])</span>
|
||||
|
||||
<span class="c1"># find all photos that include Alice Smith but do not contain the keyword Bar</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">persons</span><span class="o">=</span><span class="p">[</span><span class="s2">"Alice Smith"</span><span class="p">])</span>
|
||||
<span class="k">if</span> <span class="n">p</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">(</span><span class="n">keywords</span><span class="o">=</span><span class="p">[</span><span class="s2">"Bar"</span><span class="p">])</span> <span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="nb">print</span><span class="p">(</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">original_filename</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">date</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">description</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">title</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">albums</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">persons</span><span class="p">,</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
|
||||
<span class="n">main</span><span class="p">()</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Export all photos to specified directory using album names as folders</span>
|
||||
<span class="sd"> If file has been edited, also export the edited version,</span>
|
||||
<span class="sd"> otherwise, export the original version</span>
|
||||
<span class="sd"> This will result in duplicate photos if photo is in more than album """</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">os.path</span>
|
||||
<span class="kn">import</span> <span class="nn">pathlib</span>
|
||||
<span class="kn">import</span> <span class="nn">sys</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">click</span>
|
||||
<span class="kn">from</span> <span class="nn">pathvalidate</span> <span class="kn">import</span> <span class="n">is_valid_filepath</span><span class="p">,</span> <span class="n">sanitize_filepath</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
|
||||
|
||||
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
|
||||
<span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s2">"export_path"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">click</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">exists</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
|
||||
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
|
||||
<span class="s2">"--default-album"</span><span class="p">,</span>
|
||||
<span class="n">help</span><span class="o">=</span><span class="s2">"Default folder for photos with no album. Defaults to 'unfiled'"</span><span class="p">,</span>
|
||||
<span class="n">default</span><span class="o">=</span><span class="s2">"unfiled"</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span>
|
||||
<span class="s2">"--library-path"</span><span class="p">,</span>
|
||||
<span class="n">help</span><span class="o">=</span><span class="s2">"Path to Photos library, default to last used library"</span><span class="p">,</span>
|
||||
<span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">def</span> <span class="nf">export</span><span class="p">(</span><span class="n">export_path</span><span class="p">,</span> <span class="n">default_album</span><span class="p">,</span> <span class="n">library_path</span><span class="p">):</span>
|
||||
<span class="n">export_path</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">expanduser</span><span class="p">(</span><span class="n">export_path</span><span class="p">)</span>
|
||||
<span class="n">library_path</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">expanduser</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span> <span class="k">if</span> <span class="n">library_path</span> <span class="k">else</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">library_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photosdb</span> <span class="o">=</span> <span class="n">osxphotos</span><span class="o">.</span><span class="n">PhotosDB</span><span class="p">()</span>
|
||||
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photosdb</span><span class="o">.</span><span class="n">photos</span><span class="p">()</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||
<span class="n">albums</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">albums</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">albums</span><span class="p">:</span>
|
||||
<span class="n">albums</span> <span class="o">=</span> <span class="p">[</span><span class="n">default_album</span><span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="n">albums</span><span class="p">:</span>
|
||||
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">"exporting </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> in album </span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># make sure no invalid characters in destination path (could be in album name)</span>
|
||||
<span class="n">album_name</span> <span class="o">=</span> <span class="n">sanitize_filepath</span><span class="p">(</span><span class="n">album</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">"auto"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># create destination folder, if necessary, based on album name</span>
|
||||
<span class="n">dest_dir</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">export_path</span><span class="p">,</span> <span class="n">album_name</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># verify path is a valid path</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_valid_filepath</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">platform</span><span class="o">=</span><span class="s2">"auto"</span><span class="p">):</span>
|
||||
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid filepath </span><span class="si">{</span><span class="n">dest_dir</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># create destination dir if needed</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">isdir</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">):</span>
|
||||
<span class="n">os</span><span class="o">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># export the photo</span>
|
||||
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
|
||||
<span class="c1"># export edited version</span>
|
||||
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">,</span> <span class="n">edited</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="n">edited_name</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">p</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
|
||||
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Exported </span><span class="si">{</span><span class="n">edited_name</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="c1"># export unedited version</span>
|
||||
<span class="n">exported</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="n">dest_dir</span><span class="p">)</span>
|
||||
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Exported </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">exported</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">click</span><span class="o">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Skipping missing photo: </span><span class="si">{</span><span class="n">p</span><span class="o">.</span><span class="n">filename</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
|
||||
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="package-interface">
|
||||
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
|
||||
<div class="toctree-wrapper compound">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-persons">persons</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="indices-and-tables">
|
||||
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="#">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<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="#">Documentation overview</a><ul>
|
||||
<li>Next: <a href="cli.html" title="next chapter">osxphotos command line interface (CLI)</a></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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/index.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
106
docs/modules.html
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="./" 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" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<div class="section" id="osxphotos">
|
||||
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="toctree-wrapper compound">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</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="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>
|
||||
</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" />
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/modules.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/objects.inv
Normal file
BIN
docs/osxphotos.pdf
Normal file
1356
docs/reference.html
Normal file
BIN
docs/screencast/demo.gif
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
docs/screencast/osx-screenshot.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
296
docs/screencast/terminalizer-demo.yml
Normal file
@@ -0,0 +1,296 @@
|
||||
# how to use this file? see https://github.com/faressoft/terminalizer
|
||||
|
||||
# running commands:
|
||||
# mkdir trip
|
||||
# osxphotos export --export-by-date --from-date 2021-01-01 trip
|
||||
# du -h trip
|
||||
# find trip | head -20
|
||||
|
||||
|
||||
|
||||
# The configurations that used for the recording, feel free to edit them
|
||||
config:
|
||||
|
||||
# Specify a command to be executed
|
||||
# like `/bin/bash -l`, `ls`, or any other commands
|
||||
# the default is bash for Linux
|
||||
# or powershell.exe for Windows
|
||||
command: zsh
|
||||
|
||||
# Specify the current working directory path
|
||||
# the default is the current working directory path
|
||||
cwd: /Users/aravindo/Downloads
|
||||
|
||||
# Export additional ENV variables
|
||||
env:
|
||||
recording: true
|
||||
|
||||
# Explicitly set the number of columns
|
||||
# or use `auto` to take the current
|
||||
# number of columns of your shell
|
||||
cols: 91
|
||||
|
||||
# Explicitly set the number of rows
|
||||
# or use `auto` to take the current
|
||||
# number of rows of your shell
|
||||
rows: 20
|
||||
|
||||
# Amount of times to repeat GIF
|
||||
# If value is -1, play once
|
||||
# If value is 0, loop indefinitely
|
||||
# If value is a positive number, loop n times
|
||||
repeat: 0
|
||||
|
||||
# Quality
|
||||
# 1 - 100
|
||||
quality: 100
|
||||
|
||||
# Delay between frames in ms
|
||||
# If the value is `auto` use the actual recording delays
|
||||
frameDelay: auto
|
||||
|
||||
# Maximum delay between frames in ms
|
||||
# Ignored if the `frameDelay` isn't set to `auto`
|
||||
# Set to `auto` to prevent limiting the max idle time
|
||||
maxIdleTime: 2000
|
||||
|
||||
# The surrounding frame box
|
||||
# The `type` can be null, window, floating, or solid`
|
||||
# To hide the title use the value null
|
||||
# Don't forget to add a backgroundColor style with a null as type
|
||||
frameBox:
|
||||
type: floating
|
||||
title: ""
|
||||
style:
|
||||
border: 0px black solid
|
||||
# boxShadow: none
|
||||
# margin: 0px
|
||||
|
||||
# Add a watermark image to the rendered gif
|
||||
# You need to specify an absolute path for
|
||||
# the image on your machine or a URL, and you can also
|
||||
# add your own CSS styles
|
||||
watermark:
|
||||
imagePath: null
|
||||
style:
|
||||
position: absolute
|
||||
right: 15px
|
||||
bottom: 15px
|
||||
width: 100px
|
||||
opacity: 0.9
|
||||
|
||||
# Cursor style can be one of
|
||||
# `block`, `underline`, or `bar`
|
||||
cursorStyle: block
|
||||
|
||||
# Font family
|
||||
# You can use any font that is installed on your machine
|
||||
# in CSS-like syntax
|
||||
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"
|
||||
|
||||
# The size of the font
|
||||
fontSize: 12
|
||||
|
||||
# The height of lines
|
||||
lineHeight: 1
|
||||
|
||||
# The spacing between letters
|
||||
letterSpacing: 0
|
||||
|
||||
# Theme
|
||||
theme:
|
||||
background: "transparent"
|
||||
foreground: "#afafaf"
|
||||
cursor: "#c7c7c7"
|
||||
black: "#232628"
|
||||
red: "#fc4384"
|
||||
green: "#b3e33b"
|
||||
yellow: "#ffa727"
|
||||
blue: "#75dff2"
|
||||
magenta: "#ae89fe"
|
||||
cyan: "#708387"
|
||||
white: "#d5d5d0"
|
||||
brightBlack: "#626566"
|
||||
brightRed: "#ff7fac"
|
||||
brightGreen: "#c8ed71"
|
||||
brightYellow: "#ebdf86"
|
||||
brightBlue: "#75dff2"
|
||||
brightMagenta: "#ae89fe"
|
||||
brightCyan: "#b1c6ca"
|
||||
brightWhite: "#f9f9f4"
|
||||
|
||||
# Records, feel free to edit them
|
||||
records:
|
||||
- delay: 100
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 100
|
||||
content: m
|
||||
- delay: 100
|
||||
content: "\bmk"
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: "\e[?2004l\r\r\n"
|
||||
- delay: 9
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 300
|
||||
content: o
|
||||
- delay: 100
|
||||
content: "\bos"
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: h
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: s
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: b
|
||||
- delay: 100
|
||||
content: 'y'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: a
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: f
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: m
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: a
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '2'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '2'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 300
|
||||
content: "\e[?2004l\r\r\n"
|
||||
- delay: 500
|
||||
content: "Using last opened Photos library: /Users/user/Pictures/Photos Library.photoslibrary\r\n"
|
||||
- delay: 8204
|
||||
content: "Exporting 79 photos to /Users/user/trip...\r\n"
|
||||
- delay: 321
|
||||
content: "Processed: 79 photos, exported: 80, missing: 0, error: 0\r\nElapsed time: 0.321 seconds\r\n"
|
||||
- delay: 317
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
|
||||
- delay: 4252
|
||||
content: "\e[7mdu -h trip\e[27m"
|
||||
- delay: 487
|
||||
content: "\e[10D\e[27md\e[27mu\e[27m \e[27m-\e[27mh\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[?2004l\r\r\n"
|
||||
- delay: 7
|
||||
content: "229M\ttrip/2021/01/03\r\n712K\ttrip/2021/01/02\r\n7.5M\ttrip/2021/01/01\r\n237M\ttrip/2021/01\r\n237M\ttrip/2021\r\n238M\ttrip\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 4280
|
||||
content: "\e[7mfind trip | head -20\e[27m"
|
||||
- delay: 923
|
||||
content: "\e[20D\e[27mf\e[27mi\e[27mn\e[27md\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[27m \e[27m|\e[27m \e[27mh\e[27me\e[27ma\e[27md\e[27m \e[27m-\e[27m2\e[27m0\e[?2004l\r\r\n"
|
||||
- delay: 5
|
||||
content: "trip\r\ntrip/2021\r\ntrip/2021/01\r\ntrip/2021/01/03\r\ntrip/2021/01/03/IMG_1234 (1).HEIC\r\ntrip/2021/01/03/IMG_1267.HEIC\r\ntrip/2021/01/03/IMG_1226.HEIC\r\ntrip/2021/01/03/IMG_1271.HEIC\r\ntrip/2021/01/03/IMG_1232 (1).JPG\r\ntrip/2021/01/03/IMG_1270.HEIC\r\ntrip/2021/01/03/IMG_1231.HEIC\r\ntrip/2021/01/03/IMG_6926.JPG\r\ntrip/2021/01/03/IMG_6932.JPG\r\ntrip/2021/01/03/IMG_1266.HEIC\r\ntrip/2021/01/03/IMG_6933.JPG\r\ntrip/2021/01/03/IMG_6927.JPG\r\ntrip/2021/01/03/IMG_1233 (1).JPG\r\ntrip/2021/01/03/IMG_1228 (1).HEIC\r\ntrip/2021/01/03/IMG_6931.JPG\r\ntrip/2021/01/03/IMG_6930.JPG\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 3615
|
||||
content: "\e[?2004l\r\r\n"
|
||||
114
docs/search.html
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.40.8 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
|
||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<script src="_static/searchtools.js"></script>
|
||||
<script src="_static/language_data.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="#" />
|
||||
<script src="searchindex.js" defer></script>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1 id="search-documentation">Search</h1>
|
||||
<div id="fallback" class="admonition warning">
|
||||
<script>$('#fallback').hide();</script>
|
||||
<p>
|
||||
Please activate JavaScript to enable the search
|
||||
functionality.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Searching for multiple words only shows matches that contain
|
||||
all words.
|
||||
</p>
|
||||
<form action="" method="get">
|
||||
<input type="text" name="q" aria-labelledby="search-documentation" value="" />
|
||||
<input type="submit" value="search" />
|
||||
<span id="search-progress" style="padding-left: 10px"></span>
|
||||
</form>
|
||||
|
||||
<div id="search-results">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</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="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>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
docs/searchindex.js
Normal file
28
docsrc/Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
github:
|
||||
@make html
|
||||
@cp -a _build/html/. ../docs
|
||||
|
||||
pdf:
|
||||
@make latexpdf
|
||||
@cp -a _build/latex/osxphotos.pdf ../docs
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
13
docsrc/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Building the documentation
|
||||
|
||||
I'm still trying to learn sphinx and come up with a workflow for building docs. Right now it's pretty kludgy.
|
||||
|
||||
- `pip install sphinx`
|
||||
- `pip install sphinx-rtd-theme`
|
||||
- `pip install m2r2`
|
||||
- Download and install [MacTeX](https://tug.org/mactex/)
|
||||
- Add `/Library/TeX/texbin` to your `$PATH`
|
||||
- `cd docs`
|
||||
- `make html`
|
||||
- `make latexpdf`
|
||||
- `cp _build/latex/osxphotos.pdf .`
|
||||
35
docsrc/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
6
docsrc/source/cli.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
osxphotos command line interface (CLI)
|
||||
======================================
|
||||
|
||||
.. click:: osxphotos.cli:cli
|
||||
:prog: osxphotos
|
||||
:nested: full
|
||||
69
docsrc/source/conf.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "osxphotos"
|
||||
copyright = "2021, Rhet Turnbull"
|
||||
author = "Rhet Turnbull"
|
||||
|
||||
# holds config info read from disk
|
||||
about = {}
|
||||
this_directory = pathlib.Path(__file__).parent
|
||||
version_file = this_directory.parent.parent / "osxphotos" / "_version.py"
|
||||
# get version info from _version
|
||||
with open(
|
||||
version_file, mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = about["__version__"]
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx_click", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "m2r2"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "alabaster"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
23
docsrc/source/index.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
.. osxphotos documentation master file, created by
|
||||
sphinx-quickstart on Sat Jan 23 13:27:27 2021.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to osxphotos's documentation!
|
||||
=====================================
|
||||
|
||||
.. include:: ../../README.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
cli
|
||||
reference
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
5
docsrc/source/modules.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
osxphotos
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
13
docsrc/source/reference.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
osxphotos package
|
||||
===================
|
||||
|
||||
osxphotos module
|
||||
------------------------------
|
||||
|
||||
.. autoclass:: osxphotos.PhotosDB
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autoclass:: osxphotos.PhotoInfo
|
||||
:members:
|
||||
:undoc-members:
|
||||
@@ -15,7 +15,7 @@ import time
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import get_photos_db, _list_libraries
|
||||
from osxphotos.cli import get_photos_db, _list_libraries
|
||||
|
||||
|
||||
def show(photo):
|
||||
|
||||
@@ -8,7 +8,7 @@ import importlib
|
||||
pathex = os.getcwd()
|
||||
|
||||
# include necessary data files
|
||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates')]
|
||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
|
||||
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||
for package, files in package_imports:
|
||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||
|
||||
@@ -5,6 +5,8 @@ Constants used by osxphotos
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
|
||||
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
# Time delta: add this to Photos times to get unix time
|
||||
# Apple Epoch is Jan 1, 2001
|
||||
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
||||
@@ -58,8 +60,17 @@ _DB_TABLE_NAMES = {
|
||||
},
|
||||
}
|
||||
|
||||
# which major version operating systems have been tested
|
||||
_TESTED_OS_VERSIONS = ["12", "13", "14", "15", "16"]
|
||||
# which version operating systems have been tested
|
||||
_TESTED_OS_VERSIONS = [
|
||||
("10", "12"),
|
||||
("10", "13"),
|
||||
("10", "14"),
|
||||
("10", "15"),
|
||||
("10", "16"),
|
||||
("11", "0"),
|
||||
("11", "1"),
|
||||
("11", "2"),
|
||||
]
|
||||
|
||||
# Photos 5 has persons who are empty string if unidentified face
|
||||
_UNKNOWN_PERSON = "_UNKNOWN_"
|
||||
@@ -79,6 +90,7 @@ _MOVIE_TYPE = 1
|
||||
# Name of XMP template file
|
||||
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
||||
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
||||
_XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||
|
||||
# Constants used for processing folders and albums
|
||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
@@ -108,7 +120,7 @@ SEARCH_CATEGORY_NEIGHBORHOOD = 3
|
||||
SEARCH_CATEGORY_LOCALITY_4 = 4
|
||||
SEARCH_CATEGORY_SUB_LOCALITY_5 = 5
|
||||
SEARCH_CATEGORY_SUB_LOCALITY_6 = 6
|
||||
SEARCH_CATEGORY_CITY = 7
|
||||
SEARCH_CATEGORY_CITY = 7
|
||||
SEARCH_CATEGORY_LOCALITY_8 = 8
|
||||
SEARCH_CATEGORY_NAMED_AREA = 9
|
||||
SEARCH_CATEGORY_ALL_LOCALITY = [
|
||||
@@ -178,3 +190,23 @@ DEFAULT_ORIGINAL_SUFFIX = ""
|
||||
# Colors for print CLI messages
|
||||
CLI_COLOR_ERROR = "red"
|
||||
CLI_COLOR_WARNING = "yellow"
|
||||
|
||||
# Bit masks for --sidecar
|
||||
SIDECAR_JSON = 0x1
|
||||
SIDECAR_EXIFTOOL = 0x2
|
||||
SIDECAR_XMP = 0x4
|
||||
|
||||
# supported attributes for --xattr-template
|
||||
EXTENDED_ATTRIBUTE_NAMES = [
|
||||
"authors",
|
||||
"comment",
|
||||
"copyright",
|
||||
"description",
|
||||
"findercomment",
|
||||
"headline",
|
||||
"keywords",
|
||||
]
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
||||
|
||||
# name of export DB
|
||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||
@@ -1,5 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.38.12"
|
||||
|
||||
|
||||
__version__ = "0.40.11"
|
||||
|
||||
3568
osxphotos/cli.py
Normal file
286
osxphotos/cli_help.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Help text helper class for osxphotos CLI """
|
||||
|
||||
import click
|
||||
import osxmetadata
|
||||
|
||||
from ._constants import (
|
||||
EXTENDED_ATTRIBUTE_NAMES,
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||
OSXPHOTOS_EXPORT_DB,
|
||||
)
|
||||
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||
|
||||
|
||||
class ExportCommand(click.Command):
|
||||
""" Custom click.Command that overrides get_help() to show additional help info for export """
|
||||
|
||||
def get_help(self, ctx):
|
||||
help_text = super().get_help(ctx)
|
||||
formatter = click.HelpFormatter()
|
||||
|
||||
# passed to click.HelpFormatter.write_dl for formatting
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text("** Export **")
|
||||
formatter.write_text(
|
||||
"When exporting photos, osxphotos creates a database in the top-level "
|
||||
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
|
||||
+ "used for determining which files need to be updated when run with --update. It is recommended "
|
||||
+ "that if you later move the export folder tree you also move the database file."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"The --update option will only copy new or updated files from the library "
|
||||
+ "to the export folder. If a file is changed in the export folder (for example, you edited the "
|
||||
+ "exported image), osxphotos will detect this as a difference and re-export the original image "
|
||||
+ "from the library thus overwriting the changes. If using --update, the exported library "
|
||||
+ "should be treated as a backup, not a working copy where you intend to make changes. "
|
||||
+ "If you do edit or process the exported files and do not want them to be overwritten with"
|
||||
+ "subsequent --update, use --ignore-signature which will match filename but not file signature when "
|
||||
+ "exporting."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"Note: The number of files reported for export and the number actually exported "
|
||||
+ "may differ due to live photos, associated raw images, and edited photos which are reported "
|
||||
+ "in the total photos exported."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"Implementation note: To determine which files need to be updated, "
|
||||
+ f"osxphotos stores file signature information in the '{OSXPHOTOS_EXPORT_DB}' database. "
|
||||
+ "The signature includes size, modification time, and filename. In order to minimize "
|
||||
+ "run time, --update does not do a full comparison (diff) of the files nor does it compare "
|
||||
+ "hashes of the files. In normal usage, this is sufficient for updating the library. "
|
||||
+ "You can always run export without the --update option to re-export the entire library thus "
|
||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text("** Extended Attributes **")
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"""
|
||||
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
|
||||
additional metadata to extended attributes in the file. These options will only work
|
||||
if the destination filesystem supports extended attributes (most do).
|
||||
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
|
||||
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
|
||||
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
|
||||
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
|
||||
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
|
||||
Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services
|
||||
do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes
|
||||
will cause Dropbox to re-sync the files.
|
||||
|
||||
The following attributes may be used with '--xattr-template':
|
||||
|
||||
"""
|
||||
)
|
||||
formatter.write_dl(
|
||||
[
|
||||
(
|
||||
attr,
|
||||
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
||||
)
|
||||
for attr in EXTENDED_ATTRIBUTE_NAMES
|
||||
]
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text("** Templating System **")
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"""
|
||||
Several options, such as --directory, allow you to specify a template which
|
||||
will be rendered to substitute template fields with values from the photo.
|
||||
For example, '{created.month}' would be replaced with the month name of the
|
||||
photo creation date. e.g. 'November'.
|
||||
|
||||
Some options supporting templates may be repeated e.g., --keyword-template
|
||||
'{label}' --keyword-template '{media_type}' to add both labels and media
|
||||
types to the keywords.
|
||||
|
||||
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is:
|
||||
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
|
||||
|
||||
With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD
|
||||
is optional.
|
||||
|
||||
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place'
|
||||
with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'.
|
||||
For example, a photo with keywords 'foo' and 'bar':
|
||||
|
||||
'{keyword}' renders to 'foo' and 'bar'
|
||||
|
||||
'{,+keyword}' renders to: 'foo,bar'
|
||||
|
||||
'{; +keyword}' renders to: 'foo; bar'
|
||||
|
||||
'{+keyword}' renders to 'foobar'
|
||||
|
||||
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
|
||||
|
||||
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in
|
||||
that they join multiple elements into a single path-like string. For example,
|
||||
if photo is in album Album1 in folder Folder1, '{folder_album}' results in
|
||||
'Folder1/Album1'. This is so these template fields may be used as paths in
|
||||
--directory. If you intend to use such a field as a string, e.g. in the
|
||||
filename, you may specify a different path separator using the form:
|
||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
||||
would result in 'Folder1Album1'.
|
||||
|
||||
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value
|
||||
with text "NEW". For example, if you have album names with '/' in the album name you
|
||||
could replace '/' with "-" using the template '{album[/,-]}'. This would replace
|
||||
any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus
|
||||
become "Vacation-2019". You may specify more than one pair of OLD,NEW values by
|
||||
listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both
|
||||
'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by
|
||||
omitting the NEW value as in '{album[/,]}'.
|
||||
|
||||
- '?' Some template fields such as 'hdr' are boolean and resolve to True or False.
|
||||
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
|
||||
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image
|
||||
and 'not_hdr' otherwise.
|
||||
|
||||
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results
|
||||
in a null (empty) value, the template will result in default value of '_'.
|
||||
You may specify an alternate default value by appending ',DEFAULT' after
|
||||
template_field. Example: '{title,no_title}' would result in 'no_title' if the photo
|
||||
had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was
|
||||
no address associated with the photo, the resulting output would be:
|
||||
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
|
||||
contain a brace symbol ('{' or '}').
|
||||
|
||||
Again, if you do not specify a default value and the template substitution has no
|
||||
value, '_' (underscore) will be used as the default value. For example, in the
|
||||
above example, this would result in '2020/_/photoname.jpg' if address was
|
||||
null.
|
||||
|
||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
||||
after the comma, e.g. {title,} which would render to "" if title had no value thus
|
||||
effectively deleting the template from the resulting string.
|
||||
|
||||
You may include other text in the template string outside the {}
|
||||
and use more than one template field in a single string,
|
||||
e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
|
||||
|
||||
Some templates may resolve to more than one value. For example, a photo can
|
||||
have multiple keywords so '{keyword}' can result in multiple values. If used
|
||||
in a filename or directory, these templates may result in more than one copy
|
||||
of the photo being exported. For example, if photo has keywords "foo" and
|
||||
"bar", --directory '{keyword}' will result in copies of the photo being
|
||||
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
||||
|
||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
|
||||
customization of the output. For example, '{media_type}' resolves to the
|
||||
special media type of the photo such as 'panorama' or 'selfie'. You may use
|
||||
the 'DEFAULT' value to override these in form:
|
||||
'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if
|
||||
photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée'
|
||||
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
|
||||
ordinary video.
|
||||
|
||||
With the --directory and --filename options you may specify a template for the
|
||||
export directory or filename, respectively. The directory will be appended to
|
||||
the export path specified in the export DEST argument to export. For example,
|
||||
if template is '{created.year}/{created.month}', and export destination DEST
|
||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
||||
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
||||
March 2020.
|
||||
|
||||
The templating system may also be used with the --keyword-template option to
|
||||
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
||||
new keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||
structure, you can use --keyword-template "{folder_album}"
|
||||
|
||||
In the template, valid template substitutions will be replaced by the
|
||||
corresponding value from the table below. Invalid substitutions will result
|
||||
in an error.
|
||||
|
||||
If you want the actual text of the template substition to appear in the
|
||||
rendered name, use double braces, e.g. '{{' or '}}', thus using
|
||||
'{created.year}/{{name}}' for --directory would result in output of
|
||||
2020/{name}/photoname.jpg
|
||||
"""
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"With the --directory and --filename options you may specify a template for the "
|
||||
+ "export directory or filename, respectively. "
|
||||
+ "The directory will be appended to the export path specified "
|
||||
+ "in the export DEST argument to export. For example, if template is "
|
||||
+ "'{created.year}/{created.month}', and export destination DEST is "
|
||||
+ "'/Users/maria/Pictures/export', "
|
||||
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
|
||||
+ "if the photo was created in March 2020. "
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"The templating system may also be used with the --keyword-template option "
|
||||
+ "to set keywords on export (with --exiftool or --sidecar), "
|
||||
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
|
||||
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"'
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"In the template, valid template substitutions will be replaced by "
|
||||
+ "the corresponding value from the table below. Invalid substitutions will result in a "
|
||||
+ "an error and the script will abort."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"If you want the actual text of the template substition to appear "
|
||||
+ "in the rendered name, use double braces, e.g. '{{' or '}}', thus "
|
||||
+ "using '{created.year}/{{name}}' for --directory "
|
||||
+ "would result in output of 2020/{name}/photoname.jpg"
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"You may specify an optional default value to use if the substitution does not contain a value "
|
||||
+ "(e.g. the value is null) "
|
||||
+ "by specifying the default value after a ',' in the template string: "
|
||||
+ "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' "
|
||||
+ "but there was no address associated with the photo, the resulting output would be: "
|
||||
+ "'2020/NO_ADDRESS/photoname.jpg'. "
|
||||
+ "If specified, the default value may not contain a brace symbol ('{' or '}')."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"If you do not specify a default value and the template substitution "
|
||||
+ "has no value, '_' (underscore) will be used as the default value. For example, in the "
|
||||
+ "above example, this would result in '2020/_/photoname.jpg' if address was null."
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
'You may specify a null default (e.g. "" or empty string) by omitting the value after '
|
||||
+ 'the comma, e.g. {title,} which would render to "" if title had no value.'
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"The following substitutions may result in multiple values. Thus "
|
||||
+ "if specified for --directory these could result in multiple copies of a photo being "
|
||||
+ "being exported, one to each directory. For example: "
|
||||
+ "--directory '{created.year}/{album}' could result in the same photo being exported "
|
||||
+ "to each of the following directories if the photos were created in 2019 "
|
||||
+ "and were in albums 'Vacation' and 'Family': "
|
||||
+ "2019/Vacation, 2019/Family"
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples.extend(
|
||||
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
||||
)
|
||||
|
||||
formatter.write_dl(templ_tuples)
|
||||
help_text += formatter.getvalue()
|
||||
return help_text
|
||||
@@ -9,11 +9,11 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from functools import lru_cache # pylint: disable=syntax-error
|
||||
|
||||
|
||||
# exiftool -stay_open commands outputs this EOF marker after command is run
|
||||
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
||||
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
|
||||
@@ -33,8 +33,8 @@ def get_exiftool_path():
|
||||
|
||||
|
||||
class _ExifToolProc:
|
||||
""" Runs exiftool in a subprocess via Popen
|
||||
Creates a singleton object """
|
||||
"""Runs exiftool in a subprocess via Popen
|
||||
Creates a singleton object"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
""" create new object or return instance of already created singleton """
|
||||
@@ -44,20 +44,20 @@ class _ExifToolProc:
|
||||
return cls.instance
|
||||
|
||||
def __init__(self, exiftool=None):
|
||||
""" construct _ExifToolProc singleton object or return instance of already created object
|
||||
exiftool: optional path to exiftool binary (if not provided, will search path to find it) """
|
||||
"""construct _ExifToolProc singleton object or return instance of already created object
|
||||
exiftool: optional path to exiftool binary (if not provided, will search path to find it)"""
|
||||
|
||||
if hasattr(self, "_process_running") and self._process_running:
|
||||
# already running
|
||||
if exiftool is not None:
|
||||
if exiftool is not None and exiftool != self._exiftool:
|
||||
logging.warning(
|
||||
f"exiftool subprocess already running, "
|
||||
f"ignoring exiftool={exiftool}"
|
||||
)
|
||||
return
|
||||
|
||||
self._exiftool = exiftool or get_exiftool_path()
|
||||
self._process_running = False
|
||||
self._exiftool = exiftool or get_exiftool_path()
|
||||
self._start_proc()
|
||||
|
||||
@property
|
||||
@@ -106,8 +106,8 @@ class _ExifToolProc:
|
||||
|
||||
def _stop_proc(self):
|
||||
""" stop the exiftool process if it's running, otherwise, do nothing """
|
||||
|
||||
if not self._process_running:
|
||||
logging.warning("exiftool process is not running")
|
||||
return
|
||||
|
||||
self._process.stdin.write(b"-stay_open\n")
|
||||
@@ -133,7 +133,7 @@ class ExifTool:
|
||||
""" Basic exiftool interface for reading and writing EXIF tags """
|
||||
|
||||
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
|
||||
""" Create ExifTool object
|
||||
"""Create ExifTool object
|
||||
|
||||
Args:
|
||||
file: path to image file
|
||||
@@ -157,15 +157,15 @@ class ExifTool:
|
||||
self._read_exif()
|
||||
|
||||
def setvalue(self, tag, value):
|
||||
""" Set tag to value(s); if value is None, will delete tag
|
||||
"""Set tag to value(s); if value is None, will delete tag
|
||||
|
||||
Args:
|
||||
tag: str; name of tag to set
|
||||
value: str; value to set tag to
|
||||
|
||||
|
||||
Returns:
|
||||
True if success otherwise False
|
||||
|
||||
|
||||
If error generated by exiftool, returns False and sets self.error to error string
|
||||
If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string
|
||||
If called in context manager, returns True (execution is delayed until exiting context manager)
|
||||
@@ -184,26 +184,26 @@ class ExifTool:
|
||||
return error is None
|
||||
|
||||
def addvalues(self, tag, *values):
|
||||
""" Add one or more value(s) to tag
|
||||
"""Add one or more value(s) to tag
|
||||
If more than one value is passed, each value will be added to the tag
|
||||
|
||||
Args:
|
||||
tag: str; tag to set
|
||||
*values: str; one or more values to set
|
||||
|
||||
|
||||
Returns:
|
||||
True if success otherwise False
|
||||
|
||||
|
||||
If error generated by exiftool, returns False and sets self.error to error string
|
||||
If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string
|
||||
If called in context manager, returns True (execution is delayed until exiting context manager)
|
||||
|
||||
|
||||
Notes: exiftool may add duplicate values for some tags so the caller must ensure
|
||||
the values being added are not already in the EXIF data
|
||||
For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,
|
||||
For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,
|
||||
but for others, such as EXIF:ISO, this will literally add a value to the existing value.
|
||||
It's up to the caller to know what exiftool will do for each tag
|
||||
If setvalue called before addvalues, exiftool does not appear to add duplicates,
|
||||
If setvalue called before addvalues, exiftool does not appear to add duplicates,
|
||||
but if addvalues called without first calling setvalue, exiftool will add duplicate values
|
||||
"""
|
||||
if not values:
|
||||
@@ -226,7 +226,7 @@ class ExifTool:
|
||||
return error is None
|
||||
|
||||
def run_commands(self, *commands, no_file=False):
|
||||
""" Run commands in the exiftool process and return result.
|
||||
"""Run commands in the exiftool process and return result.
|
||||
|
||||
Args:
|
||||
*commands: exiftool commands to run
|
||||
@@ -254,7 +254,11 @@ class ExifTool:
|
||||
filename = os.fsencode(self.file) if not no_file else b""
|
||||
|
||||
if self.flags:
|
||||
command_str = b"\n".join([f.encode("utf-8") for f in self.flags])
|
||||
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
|
||||
flags = []
|
||||
for f in self.flags:
|
||||
flags.extend(f.split())
|
||||
command_str = b"\n".join([f.encode("utf-8") for f in flags])
|
||||
command_str += b"\n"
|
||||
else:
|
||||
command_str = b""
|
||||
@@ -266,7 +270,7 @@ class ExifTool:
|
||||
+ b"\n"
|
||||
+ b"-execute\n"
|
||||
)
|
||||
|
||||
|
||||
# send the command
|
||||
self._process.stdin.write(command_str)
|
||||
self._process.stdin.flush()
|
||||
@@ -300,17 +304,28 @@ class ExifTool:
|
||||
ver, _, _ = self.run_commands("-ver", no_file=True)
|
||||
return ver.decode("utf-8")
|
||||
|
||||
def asdict(self):
|
||||
""" return dictionary of all EXIF tags and values from exiftool
|
||||
returns empty dict if no tags
|
||||
def asdict(self, tag_groups=True):
|
||||
"""return dictionary of all EXIF tags and values from exiftool
|
||||
returns empty dict if no tags
|
||||
|
||||
Args:
|
||||
tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"
|
||||
"""
|
||||
json_str, _, _ = self.run_commands("-json")
|
||||
if json_str:
|
||||
exifdict = json.loads(json_str)
|
||||
return exifdict[0]
|
||||
else:
|
||||
if not json_str:
|
||||
return dict()
|
||||
|
||||
exifdict = json.loads(json_str)
|
||||
exifdict = exifdict[0]
|
||||
if not tag_groups:
|
||||
# strip tag groups
|
||||
exif_new = {}
|
||||
for k, v in exifdict.items():
|
||||
k = re.sub(r".*:", "", k)
|
||||
exif_new[k] = v
|
||||
exifdict = exif_new
|
||||
return exifdict
|
||||
|
||||
def json(self):
|
||||
""" returns JSON string containing all EXIF tags and values from exiftool """
|
||||
json, _, _ = self.run_commands("-json")
|
||||
|
||||
@@ -84,6 +84,10 @@ class ExportDB_ABC(ABC):
|
||||
def get_sidecar_for_file(self, filename):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_previous_uuids(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_data(
|
||||
self,
|
||||
@@ -155,6 +159,9 @@ class ExportDBNoOp(ExportDB_ABC):
|
||||
def get_sidecar_for_file(self, filename):
|
||||
return None, (None, None, None)
|
||||
|
||||
def get_previous_uuids(self):
|
||||
return []
|
||||
|
||||
def set_data(
|
||||
self,
|
||||
filename,
|
||||
@@ -435,6 +442,19 @@ class ExportDB(ExportDB_ABC):
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
|
||||
def get_previous_uuids(self):
|
||||
"""returns list of UUIDs of previously exported photos found in export database """
|
||||
conn = self._conn
|
||||
previous_uuids = []
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT DISTINCT uuid FROM files")
|
||||
results = c.fetchall()
|
||||
previous_uuids = [row[0] for row in results]
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
return previous_uuids
|
||||
|
||||
def set_data(
|
||||
self,
|
||||
filename,
|
||||
|
||||
@@ -60,6 +60,11 @@ class FileUtilABC(ABC):
|
||||
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def rename(cls, src, dest):
|
||||
pass
|
||||
|
||||
|
||||
class FileUtilMacOS(FileUtilABC):
|
||||
""" Various file utilities """
|
||||
@@ -201,6 +206,21 @@ class FileUtilMacOS(FileUtilABC):
|
||||
src_file, dest_file, compression_quality=compression_quality
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def rename(cls, src, dest):
|
||||
""" Copy src to dest
|
||||
|
||||
Args:
|
||||
src: path to source file
|
||||
dest: path to destination file
|
||||
|
||||
Returns:
|
||||
Name of renamed file (dest)
|
||||
|
||||
"""
|
||||
os.rename(str(src), str(dest))
|
||||
return dest
|
||||
|
||||
@staticmethod
|
||||
def _sig(st):
|
||||
""" return tuple of (mode, size, mtime) of file based on os.stat
|
||||
@@ -266,3 +286,7 @@ class FileUtilNoOp(FileUtil):
|
||||
@classmethod
|
||||
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
||||
cls.verbose(f"convert_to_jpeg: {src_file}, {dest_file}, {compression_quality}")
|
||||
|
||||
@classmethod
|
||||
def rename(cls, src, dest):
|
||||
cls.verbose(f"rename: {src}, {dest}")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
""" ImageConverter class
|
||||
Convert an image to JPEG using CoreImage --
|
||||
Convert an image to JPEG using CoreImage --
|
||||
for example, RAW to JPEG. Only works if Mac equipped with GPU. """
|
||||
|
||||
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
import objc
|
||||
import Metal
|
||||
import Quartz
|
||||
from Cocoa import NSURL
|
||||
@@ -16,6 +16,12 @@ from Foundation import NSDictionary
|
||||
from wurlitzer import pipes
|
||||
|
||||
|
||||
class ImageConversionError(Exception):
|
||||
"""Base class for exceptions in this module. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
""" Convert images to jpeg. This class is a singleton
|
||||
which will re-use the Core Image CIContext to avoid
|
||||
@@ -60,53 +66,61 @@ class ImageConverter:
|
||||
Raises:
|
||||
ValueError if compression quality not in range 0.0 to 1.0
|
||||
FileNotFoundError if input_path doesn't exist
|
||||
ImageConversionError if error during conversion
|
||||
"""
|
||||
|
||||
# accept input_path or output_path as pathlib.Path
|
||||
if not isinstance(input_path, str):
|
||||
input_path = str(input_path)
|
||||
# Set up a dedicated objc autorelease pool for this function call.
|
||||
# This is to ensure that all the NSObjects are cleaned up after each
|
||||
# call to prevent memory leaks.
|
||||
# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
|
||||
# https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management
|
||||
with objc.autorelease_pool():
|
||||
# accept input_path or output_path as pathlib.Path
|
||||
if not isinstance(input_path, str):
|
||||
input_path = str(input_path)
|
||||
|
||||
if not isinstance(output_path, str):
|
||||
output_path = str(output_path)
|
||||
if not isinstance(output_path, str):
|
||||
output_path = str(output_path)
|
||||
|
||||
if not pathlib.Path(input_path).is_file():
|
||||
raise FileNotFoundError(f"could not find {input_path}")
|
||||
if not pathlib.Path(input_path).is_file():
|
||||
raise FileNotFoundError(f"could not find {input_path}")
|
||||
|
||||
if not (0.0 <= compression_quality <= 1.0):
|
||||
raise ValueError(
|
||||
"illegal value for compression_quality: {compression_quality}"
|
||||
if not (0.0 <= compression_quality <= 1.0):
|
||||
raise ValueError(
|
||||
"illegal value for compression_quality: {compression_quality}"
|
||||
)
|
||||
|
||||
input_url = NSURL.fileURLWithPath_(input_path)
|
||||
output_url = NSURL.fileURLWithPath_(output_path)
|
||||
|
||||
with pipes() as (out, err):
|
||||
# capture stdout and stderr from system calls
|
||||
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
|
||||
# prints to stderr something like:
|
||||
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
|
||||
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
if input_image is None:
|
||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
)
|
||||
|
||||
input_url = NSURL.fileURLWithPath_(input_path)
|
||||
output_url = NSURL.fileURLWithPath_(output_path)
|
||||
|
||||
with pipes() as (out, err):
|
||||
# capture stdout and stderr from system calls
|
||||
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
|
||||
# prints to stderr something like:
|
||||
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
|
||||
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
if input_image is None:
|
||||
logging.debug(f"Could not create CIImage for {input_path}")
|
||||
return False
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
)
|
||||
|
||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
|
||||
)
|
||||
_, error = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
|
||||
input_image, output_url, output_colorspace, output_options, None
|
||||
)
|
||||
if not error:
|
||||
return True
|
||||
else:
|
||||
logging.debug(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
|
||||
)
|
||||
return False
|
||||
(
|
||||
_,
|
||||
error,
|
||||
) = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
|
||||
input_image, output_url, output_colorspace, output_options, None
|
||||
)
|
||||
if not error:
|
||||
return True
|
||||
else:
|
||||
raise ImageConversionError(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ import json
|
||||
import logging
|
||||
import math
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
|
||||
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
||||
|
||||
|
||||
class PersonInfo:
|
||||
""" Info about a person in the Photos library
|
||||
@@ -216,6 +221,62 @@ class FaceInfo:
|
||||
logging.warning(f"Could not get photo for uuid: {self.asset_uuid}")
|
||||
return self._photo
|
||||
|
||||
@property
|
||||
def mwg_rs_area(self):
|
||||
""" Get coordinates for Metadata Working Group Region Area.
|
||||
|
||||
Returns:
|
||||
MWG_RS_Area named tuple with x, y, h, w where:
|
||||
x = stArea:x
|
||||
y = stArea:y
|
||||
h = stArea:h
|
||||
w = stArea:w
|
||||
|
||||
Reference:
|
||||
https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region
|
||||
"""
|
||||
x, y = self.center_x, self.center_y
|
||||
x, y = self._fix_orientation((x, y))
|
||||
|
||||
if self.photo.orientation in [5, 6, 7, 8]:
|
||||
w = self.size_pixels / self.photo.height
|
||||
h = self.size_pixels / self.photo.width
|
||||
else:
|
||||
h = self.size_pixels / self.photo.height
|
||||
w = self.size_pixels / self.photo.width
|
||||
|
||||
return MWG_RS_Area(x, y, h, w)
|
||||
|
||||
@property
|
||||
def mpri_reg_rect(self):
|
||||
""" Get coordinates for Microsoft Photo Region Rectangle.
|
||||
|
||||
Returns:
|
||||
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
||||
x = x coordinate of top left corner of rectangle
|
||||
y = y coordinate of top left corner of rectangle
|
||||
h = height of rectangle
|
||||
w = width of rectangle
|
||||
|
||||
Reference:
|
||||
https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging
|
||||
"""
|
||||
x, y = self.center_x, self.center_y
|
||||
x, y = self._fix_orientation((x, y))
|
||||
|
||||
if self.photo.orientation in [5, 6, 7, 8]:
|
||||
w = self.size_pixels / self.photo.width
|
||||
h = self.size_pixels / self.photo.height
|
||||
x = x - self.size_pixels / self.photo.height / 2
|
||||
y = y - self.size_pixels / self.photo.width / 2
|
||||
else:
|
||||
h = self.size_pixels / self.photo.width
|
||||
w = self.size_pixels / self.photo.height
|
||||
x = x - self.size_pixels / self.photo.width / 2
|
||||
y = y - self.size_pixels / self.photo.height / 2
|
||||
|
||||
return MPRI_Reg_Rect(x, y, h, w)
|
||||
|
||||
def face_rect(self):
|
||||
""" Get face rectangle coordinates for current version of the associated image
|
||||
If image has been edited, rectangle applies to edited version, otherwise original version
|
||||
@@ -259,6 +320,43 @@ class FaceInfo:
|
||||
_, _, yaw = self.roll_pitch_yaw()
|
||||
return yaw
|
||||
|
||||
def _fix_orientation(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
|
||||
Arguments:
|
||||
xy: tuple of (x, y) coordinates for point to translate
|
||||
in format used by Photos (percent of height/width)
|
||||
|
||||
Returns:
|
||||
(x, y) tuple of translated coordinates
|
||||
"""
|
||||
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
|
||||
|
||||
orientation = self.photo.orientation
|
||||
x, y = xy
|
||||
if orientation == 1:
|
||||
y = 1.0 - y
|
||||
elif orientation == 2:
|
||||
y = 1.0 - y
|
||||
x = 1.0 - x
|
||||
elif orientation == 3:
|
||||
x = 1.0 - x
|
||||
elif orientation == 4:
|
||||
pass
|
||||
elif orientation == 5:
|
||||
x, y = 1.0 - y, x
|
||||
elif orientation == 6:
|
||||
x, y = 1.0 - y, 1.0 - x
|
||||
elif orientation == 7:
|
||||
x, y = y, x
|
||||
y = 1.0 - y
|
||||
elif orientation ==8:
|
||||
x, y = y, x
|
||||
else:
|
||||
logging.warning(f"Unhandled orientation: {orientation}")
|
||||
|
||||
return (x, y)
|
||||
|
||||
def _make_point(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
and convert to image coordinates
|
||||
@@ -273,22 +371,11 @@ class FaceInfo:
|
||||
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
|
||||
|
||||
orientation = self.photo.orientation
|
||||
x, y = xy
|
||||
x, y = self._fix_orientation(xy)
|
||||
dx = self.photo.width
|
||||
dy = self.photo.height
|
||||
if orientation in [1, 2]:
|
||||
y = 1.0 - y
|
||||
elif orientation in [3, 4]:
|
||||
x = 1.0 - x
|
||||
elif orientation in [5, 6]:
|
||||
x, y = 1.0 - y, 1.0 - x
|
||||
if orientation in [5, 6, 7, 8]:
|
||||
dx, dy = dy, dx
|
||||
elif orientation in [7, 8]:
|
||||
x, y = y, x
|
||||
dx, dy = dy, dx
|
||||
else:
|
||||
logging.warning(f"Unhandled orientation: {orientation}")
|
||||
|
||||
return (int(x * dx), int(y * dy))
|
||||
|
||||
def _make_point_with_rotation(self, xy):
|
||||
@@ -336,6 +423,8 @@ class FaceInfo:
|
||||
"right_eye": self.right_eye,
|
||||
"size": self.size,
|
||||
"face_rect": self.face_rect(),
|
||||
"mpri_reg_rect": self.mpri_reg_rect._asdict(),
|
||||
"mwg_rs_area": self.mwg_rs_area._asdict(),
|
||||
"roll": roll,
|
||||
"pitch": pitch,
|
||||
"yaw": yaw,
|
||||
|
||||
@@ -18,12 +18,11 @@ def exiftool(self):
|
||||
return self._exiftool
|
||||
except AttributeError:
|
||||
try:
|
||||
exiftool_path = get_exiftool_path()
|
||||
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
||||
if self.path is not None and os.path.isfile(self.path):
|
||||
exiftool = ExifTool(self.path)
|
||||
exiftool = ExifTool(self.path, exiftool=exiftool_path)
|
||||
else:
|
||||
exiftool = None
|
||||
logging.debug(f"exiftool: missing path {self.uuid}")
|
||||
except FileNotFoundError:
|
||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||
exiftool = None
|
||||
|
||||
@@ -21,11 +21,11 @@ from .._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_ROOT_FOLDER,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
)
|
||||
from ..albuminfo import AlbumInfo, ImportInfo
|
||||
from ..personinfo import FaceInfo, PersonInfo
|
||||
@@ -56,6 +56,8 @@ class PhotoInfo:
|
||||
_export_photo,
|
||||
_exiftool_dict,
|
||||
_exiftool_json_sidecar,
|
||||
_get_exif_keywords,
|
||||
_get_exif_persons,
|
||||
_write_exif_data,
|
||||
_write_sidecar,
|
||||
_xmp_sidecar,
|
||||
@@ -68,6 +70,7 @@ class PhotoInfo:
|
||||
self._uuid = uuid
|
||||
self._info = info
|
||||
self._db = db
|
||||
self._verbose = self._db._verbose
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
@@ -84,8 +87,8 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def original_filename(self):
|
||||
""" original filename of the picture
|
||||
Photos 5 mangles filenames upon import """
|
||||
"""original filename of the picture
|
||||
Photos 5 mangles filenames upon import"""
|
||||
if (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
@@ -104,22 +107,22 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def date_modified(self):
|
||||
""" image modification date as timezone aware datetime object
|
||||
or None if no modification date set """
|
||||
"""image modification date as timezone aware datetime object
|
||||
or None if no modification date set"""
|
||||
|
||||
# Photos <= 4 provides no way to get date of adjustment and will update
|
||||
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
|
||||
# only report lastmodified date for Photos <=4 if photo is edited;
|
||||
# even in this case, the date could be incorrect
|
||||
if self.hasadjustments or self._db._db_version > _PHOTOS_4_VERSION:
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
if not self.hasadjustments and self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -136,6 +139,7 @@ class PhotoInfo:
|
||||
except AttributeError:
|
||||
self._path = None
|
||||
photopath = None
|
||||
# TODO: should path try to return path even if ismissing?
|
||||
if self._info["isMissing"] == 1:
|
||||
return photopath # path would be meaningless until downloaded
|
||||
|
||||
@@ -490,46 +494,61 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def ismissing(self):
|
||||
""" returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
|
||||
"""returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
|
||||
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
|
||||
do not immediately get written to disk. In particular, I've noticed that downloading
|
||||
do not immediately get written to disk. In particular, I've noticed that downloading
|
||||
an image from the cloud does not force the database to be updated until something else
|
||||
e.g. an edit, keyword, etc. occurs forcing a database synch
|
||||
The exact process / timing is a mystery to be but be aware that if some photos were recently
|
||||
downloaded from cloud to local storate their status in the database might still show
|
||||
isMissing = 1
|
||||
"""
|
||||
return True if self._info["isMissing"] == 1 else False
|
||||
return self._info["isMissing"] == 1
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
""" True if picture has adjustments / edits """
|
||||
return True if self._info["hasAdjustments"] == 1 else False
|
||||
return self._info["hasAdjustments"] == 1
|
||||
|
||||
@property
|
||||
def external_edit(self):
|
||||
""" Returns True if picture was edited outside of Photos using external editor """
|
||||
return (
|
||||
True
|
||||
if self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
else False
|
||||
)
|
||||
return self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
""" True if picture is marked as favorite """
|
||||
return True if self._info["favorite"] == 1 else False
|
||||
return self._info["favorite"] == 1
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" True if picture is hidden """
|
||||
return True if self._info["hidden"] == 1 else False
|
||||
return self._info["hidden"] == 1
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
""" True if picture is visble """
|
||||
return self._info["visible"]
|
||||
|
||||
@property
|
||||
def intrash(self):
|
||||
""" True if picture is in trash ('Recently Deleted' folder)"""
|
||||
return self._info["intrash"]
|
||||
|
||||
@property
|
||||
def date_trashed(self):
|
||||
""" Date asset was placed in the trash or None """
|
||||
# TODO: add add_timezone(dt, offset_seconds) to datetime_utils
|
||||
# also update date_modified
|
||||
trasheddate = self._info["trasheddate"]
|
||||
if trasheddate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return trasheddate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
@@ -537,8 +556,8 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def shared(self):
|
||||
""" returns True if photos is in a shared iCloud album otherwise false
|
||||
Only valid on Photos 5; returns None on older versions """
|
||||
"""returns True if photos is in a shared iCloud album otherwise false
|
||||
Only valid on Photos 5; returns None on older versions"""
|
||||
if self._db._db_version > _PHOTOS_4_VERSION:
|
||||
return self._info["shared"]
|
||||
else:
|
||||
@@ -546,24 +565,25 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def uti(self):
|
||||
""" Returns Uniform Type Identifier (UTI) for the image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""Returns Uniform Type Identifier (UTI) for the image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif self.has_raw and self.raw_original:
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
and self.raw_original
|
||||
):
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
|
||||
@property
|
||||
def uti_original(self):
|
||||
""" Returns Uniform Type Identifier (UTI) for the original image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""Returns Uniform Type Identifier (UTI) for the original image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
@@ -575,9 +595,9 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def uti_edited(self):
|
||||
""" Returns Uniform Type Identifier (UTI) for the edited image
|
||||
if the photo has been edited, otherwise None;
|
||||
for example: public.jpeg
|
||||
"""Returns Uniform Type Identifier (UTI) for the edited image
|
||||
if the photo has been edited, otherwise None;
|
||||
for example: public.jpeg
|
||||
"""
|
||||
if self._db._db_version >= _PHOTOS_5_VERSION:
|
||||
return self.uti if self.hasadjustments else None
|
||||
@@ -586,36 +606,34 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def uti_raw(self):
|
||||
""" Returns Uniform Type Identifier (UTI) for the RAW image if there is one
|
||||
for example: com.canon.cr2-raw-image
|
||||
Returns None if no associated RAW image
|
||||
"""Returns Uniform Type Identifier (UTI) for the RAW image if there is one
|
||||
for example: com.canon.cr2-raw-image
|
||||
Returns None if no associated RAW image
|
||||
"""
|
||||
return self._info["UTI_raw"]
|
||||
|
||||
@property
|
||||
def ismovie(self):
|
||||
""" Returns True if file is a movie, otherwise False
|
||||
"""
|
||||
return True if self._info["type"] == _MOVIE_TYPE else False
|
||||
"""Returns True if file is a movie, otherwise False"""
|
||||
return self._info["type"] == _MOVIE_TYPE
|
||||
|
||||
@property
|
||||
def isphoto(self):
|
||||
""" Returns True if file is an image, otherwise False
|
||||
"""
|
||||
return True if self._info["type"] == _PHOTO_TYPE else False
|
||||
"""Returns True if file is an image, otherwise False"""
|
||||
return self._info["type"] == _PHOTO_TYPE
|
||||
|
||||
@property
|
||||
def incloud(self):
|
||||
""" Returns True if photo is cloud asset and is synched to cloud
|
||||
False if photo is cloud asset and not yet synched to cloud
|
||||
None if photo is not cloud asset
|
||||
"""Returns True if photo is cloud asset and is synched to cloud
|
||||
False if photo is cloud asset and not yet synched to cloud
|
||||
None if photo is not cloud asset
|
||||
"""
|
||||
return self._info["incloud"]
|
||||
|
||||
@property
|
||||
def iscloudasset(self):
|
||||
""" Returns True if photo is a cloud asset (in an iCloud library),
|
||||
otherwise False
|
||||
"""Returns True if photo is a cloud asset (in an iCloud library),
|
||||
otherwise False
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return (
|
||||
@@ -627,6 +645,11 @@ class PhotoInfo:
|
||||
else:
|
||||
return True if self._info["cloudAssetGUID"] is not None else False
|
||||
|
||||
@property
|
||||
def isreference(self):
|
||||
""" Returns True if photo is a reference (not copied to the Photos library), otherwise False """
|
||||
return self._info["isreference"]
|
||||
|
||||
@property
|
||||
def burst(self):
|
||||
""" Returns True if photo is part of a Burst photo set, otherwise False """
|
||||
@@ -634,9 +657,9 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def burst_photos(self):
|
||||
""" If photo is a burst photo, returns list of PhotoInfo objects
|
||||
that are part of the same burst photo set; otherwise returns empty list.
|
||||
self is not included in the returned list """
|
||||
"""If photo is a burst photo, returns list of PhotoInfo objects
|
||||
that are part of the same burst photo set; otherwise returns empty list.
|
||||
self is not included in the returned list"""
|
||||
if self._info["burst"]:
|
||||
burst_uuid = self._info["burstUUID"]
|
||||
return [
|
||||
@@ -654,9 +677,9 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def path_live_photo(self):
|
||||
""" Returns path to the associated video file for a live photo
|
||||
If photo is not a live photo, returns None
|
||||
If photo is missing, returns None """
|
||||
"""Returns path to the associated video file for a live photo
|
||||
If photo is not a live photo, returns None
|
||||
If photo is missing, returns None"""
|
||||
|
||||
photopath = None
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
@@ -783,9 +806,9 @@ class PhotoInfo:
|
||||
|
||||
@property
|
||||
def raw_original(self):
|
||||
""" returns True if associated raw image and the raw image is selected in Photos
|
||||
via "Use RAW as Original "
|
||||
otherwise returns False """
|
||||
"""returns True if associated raw image and the raw image is selected in Photos
|
||||
via "Use RAW as Original "
|
||||
otherwise returns False"""
|
||||
return self._info["raw_is_original"]
|
||||
|
||||
@property
|
||||
@@ -832,27 +855,27 @@ class PhotoInfo:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
strip=False,
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
Args:
|
||||
template_str: a template string with fields to render
|
||||
none_str: a str to use if template field renders to None, default is "_".
|
||||
path_sep: a single character str to use as path separator when joining
|
||||
path_sep: a single character str to use as path separator when joining
|
||||
fields like folder_album; if not provided, defaults to os.path.sep
|
||||
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||
instead of returning individual strings
|
||||
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
strip: if True, strips leading/trailing white space from resulting template
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
"""
|
||||
template = PhotoTemplate(self)
|
||||
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
||||
return template.render(
|
||||
template_str,
|
||||
none_str=none_str,
|
||||
@@ -861,7 +884,7 @@ class PhotoInfo:
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
strip=strip,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -875,11 +898,11 @@ class PhotoInfo:
|
||||
return self._info["latitude"]
|
||||
|
||||
def _get_album_uuids(self):
|
||||
""" Return list of album UUIDs this photo is found in
|
||||
|
||||
"""Return list of album UUIDs this photo is found in
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
|
||||
Returns: list of album UUIDs
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
version4 = True
|
||||
@@ -1017,6 +1040,7 @@ class PhotoInfo:
|
||||
"path_live_photo": self.path_live_photo,
|
||||
"iscloudasset": self.iscloudasset,
|
||||
"incloud": self.incloud,
|
||||
"isreference": self.isreference,
|
||||
"date_modified": self.date_modified,
|
||||
"portrait": self.portrait,
|
||||
"screenshot": self.screenshot,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# add original=False to export instead of version= (and maybe others like path())
|
||||
# make burst/live methods get uuid from self instead of passing as arg
|
||||
|
||||
import copy
|
||||
import pathlib
|
||||
import threading
|
||||
import time
|
||||
@@ -169,12 +170,14 @@ class ImageData:
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.metadata = None
|
||||
self.uti = None
|
||||
self.image_data = None
|
||||
self.info = None
|
||||
self.orientation = None
|
||||
def __init__(
|
||||
self, metadata=None, uti=None, image_data=None, info=None, orientation=None
|
||||
):
|
||||
self.metadata = metadata
|
||||
self.uti = uti
|
||||
self.image_data = image_data
|
||||
self.info = info
|
||||
self.orientation = orientation
|
||||
|
||||
|
||||
class AVAssetData:
|
||||
@@ -475,44 +478,51 @@ class PhotoAsset:
|
||||
# if self.live:
|
||||
# raise NotImplementedError("Live photos not implemented yet")
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
imagedata = self._request_image_data(version=version)
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
imagedata = self._request_image_data(version=version)
|
||||
if not imagedata.image_data:
|
||||
raise PhotoKitExportError("Could not get image data")
|
||||
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(imagedata.image_data)
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(imagedata.image_data)
|
||||
del imagedata
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
return [str(output_file)]
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
|
||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
""" Request image data and metadata for self._phasset
|
||||
@@ -523,56 +533,65 @@ class PhotoAsset:
|
||||
PHOTOS_VERSION_CURRENT, request current version with all edits
|
||||
PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version
|
||||
|
||||
Returns:
|
||||
ImageData instance
|
||||
|
||||
Raises:
|
||||
ValueError if passed invalid value for version
|
||||
"""
|
||||
|
||||
# reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc
|
||||
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
with objc.autorelease_pool():
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
|
||||
# pylint: disable=no-member
|
||||
options_request = Photos.PHImageRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setSynchronous_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = ImageData()
|
||||
event = threading.Event()
|
||||
|
||||
def handler(imageData, dataUTI, orientation, info):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
nonlocal requestdata
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
options_request = Photos.PHImageRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setSynchronous_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata.uti = dataUTI
|
||||
requestdata.orientation = orientation
|
||||
requestdata.info = info
|
||||
requestdata.image_data = imageData
|
||||
requestdata = ImageData()
|
||||
event = threading.Event()
|
||||
|
||||
event.set()
|
||||
def handler(imageData, dataUTI, orientation, info):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
self._imagedata = requestdata
|
||||
return requestdata
|
||||
nonlocal requestdata
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
)
|
||||
requestdata.uti = dataUTI
|
||||
requestdata.orientation = orientation
|
||||
requestdata.info = info
|
||||
requestdata.image_data = imageData
|
||||
|
||||
event.set()
|
||||
|
||||
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
# options_request.dealloc()
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
def _make_result_handle_(self, data):
|
||||
""" Make handler function and threading event to use with
|
||||
@@ -634,37 +653,41 @@ class SlowMoVideoExporter(NSObject):
|
||||
Returns:
|
||||
path to exported file
|
||||
"""
|
||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
)
|
||||
exporter.setOutputURL_(self.url)
|
||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||
exporter.setShouldOptimizeForNetworkUse_(True)
|
||||
|
||||
self.done = False
|
||||
with objc.autorelease_pool():
|
||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
)
|
||||
exporter.setOutputURL_(self.url)
|
||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||
exporter.setShouldOptimizeForNetworkUse_(True)
|
||||
|
||||
def handler():
|
||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||
self.done = True
|
||||
self.done = False
|
||||
|
||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||
# wait for export to complete
|
||||
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
||||
# but I can't figure out how to make that work and this does work
|
||||
while True:
|
||||
status = exporter.status()
|
||||
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
||||
break
|
||||
elif status not in (
|
||||
AVFoundation.AVAssetExportSessionStatusWaiting,
|
||||
AVFoundation.AVAssetExportSessionStatusExporting,
|
||||
):
|
||||
raise PhotoKitExportError(
|
||||
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
||||
)
|
||||
time.sleep(MIN_SLEEP)
|
||||
def handler():
|
||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||
self.done = True
|
||||
|
||||
return NSURL_to_path(exporter.outputURL())
|
||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||
# wait for export to complete
|
||||
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
||||
# but I can't figure out how to make that work and this does work
|
||||
while True:
|
||||
status = exporter.status()
|
||||
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
||||
break
|
||||
elif status not in (
|
||||
AVFoundation.AVAssetExportSessionStatusWaiting,
|
||||
AVFoundation.AVAssetExportSessionStatusExporting,
|
||||
):
|
||||
raise PhotoKitExportError(
|
||||
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
||||
)
|
||||
time.sleep(MIN_SLEEP)
|
||||
|
||||
exported_path = NSURL_to_path(exporter.outputURL())
|
||||
# exporter.dealloc()
|
||||
return exported_path
|
||||
|
||||
def __del__(self):
|
||||
self.avasset = None
|
||||
@@ -701,39 +724,43 @@ class VideoAsset(PhotoAsset):
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest, filename=filename, version=version, overwrite=overwrite
|
||||
)
|
||||
]
|
||||
with objc.autorelease_pool():
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest, filename=filename, version=version, overwrite=overwrite
|
||||
)
|
||||
]
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
del videodata
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
return [str(output_file)]
|
||||
|
||||
def _export_slow_mo(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
@@ -752,33 +779,38 @@ class VideoAsset(PhotoAsset):
|
||||
Raises:
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
if not self.slow_mo:
|
||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||
with objc.autorelease_pool():
|
||||
if not self.slow_mo:
|
||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||
|
||||
videodata = self._request_video_data(version=version)
|
||||
if (
|
||||
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
||||
or len(videodata.asset.tracks()) != 2
|
||||
):
|
||||
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
||||
videodata = self._request_video_data(version=version)
|
||||
if (
|
||||
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
||||
or len(videodata.asset.tracks()) != 2
|
||||
):
|
||||
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = dest / f"{filename.stem}.mov"
|
||||
output_file = dest / f"{filename.stem}.mov"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
||||
videodata.asset, output_file
|
||||
)
|
||||
return exporter.exportSlowMoVideo()
|
||||
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
||||
videodata.asset, output_file
|
||||
)
|
||||
video = exporter.exportSlowMoVideo()
|
||||
# exporter.dealloc()
|
||||
return video
|
||||
|
||||
# todo: rewrite this with NotificationCenter and App event loop?
|
||||
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
@@ -793,38 +825,43 @@ class VideoAsset(PhotoAsset):
|
||||
Raises:
|
||||
ValueError if passed invalid value for version
|
||||
"""
|
||||
with objc.autorelease_pool():
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = AVAssetData()
|
||||
event = threading.Event()
|
||||
|
||||
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = AVAssetData()
|
||||
event = threading.Event()
|
||||
def handler(asset, audiomix, info):
|
||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||
nonlocal requestdata
|
||||
|
||||
def handler(asset, audiomix, info):
|
||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||
nonlocal requestdata
|
||||
requestdata.asset = asset
|
||||
requestdata.audiomix = audiomix
|
||||
requestdata.info = info
|
||||
|
||||
requestdata.asset = asset
|
||||
requestdata.audiomix = audiomix
|
||||
requestdata.info = info
|
||||
event.set()
|
||||
|
||||
event.set()
|
||||
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
|
||||
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
return requestdata
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
|
||||
class LivePhotoRequest(NSObject):
|
||||
@@ -843,47 +880,54 @@ class LivePhotoRequest(NSObject):
|
||||
|
||||
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" return the photos and video components of a live video as [PHAssetResource] """
|
||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
options.setVersion_(version)
|
||||
options.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
delegate = PhotoKitNotificationDelegate.alloc().init()
|
||||
|
||||
self.nc.addObserver_selector_name_object_(
|
||||
delegate, "liveNotification:", None, None
|
||||
)
|
||||
|
||||
self.live_photo = None
|
||||
|
||||
def handler(result, info):
|
||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||
if not info["PHImageResultIsDegradedKey"]:
|
||||
self.live_photo = result
|
||||
self.info = info
|
||||
self.nc.postNotificationName_object_(
|
||||
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
||||
)
|
||||
|
||||
try:
|
||||
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
self.asset,
|
||||
Photos.PHImageManagerMaximumSize,
|
||||
Photos.PHImageContentModeDefault,
|
||||
options,
|
||||
handler,
|
||||
with objc.autorelease_pool():
|
||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
options.setVersion_(version)
|
||||
options.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
||||
except KeyboardInterrupt:
|
||||
AppHelper.stopEventLoop()
|
||||
finally:
|
||||
pass
|
||||
delegate = PhotoKitNotificationDelegate.alloc().init()
|
||||
|
||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||
self.live_photo
|
||||
)
|
||||
return asset_resources
|
||||
self.nc.addObserver_selector_name_object_(
|
||||
delegate, "liveNotification:", None, None
|
||||
)
|
||||
|
||||
self.live_photo = None
|
||||
|
||||
def handler(result, info):
|
||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||
if not info["PHImageResultIsDegradedKey"]:
|
||||
self.live_photo = result
|
||||
self.info = info
|
||||
self.nc.postNotificationName_object_(
|
||||
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
||||
)
|
||||
|
||||
try:
|
||||
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
self.asset,
|
||||
Photos.PHImageManagerMaximumSize,
|
||||
Photos.PHImageContentModeDefault,
|
||||
options,
|
||||
handler,
|
||||
)
|
||||
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
||||
except KeyboardInterrupt:
|
||||
AppHelper.stopEventLoop()
|
||||
finally:
|
||||
pass
|
||||
|
||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||
self.live_photo
|
||||
)
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(asset_resources)
|
||||
del asset_resources
|
||||
return data
|
||||
|
||||
def __del__(self):
|
||||
self.manager = None
|
||||
@@ -923,88 +967,99 @@ class LivePhotoAsset(PhotoAsset):
|
||||
ValueError if dest is not a valid directory
|
||||
PhotoKitExportError if error during export
|
||||
"""
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
photo_ext = get_preferred_uti_extension(photo_resource.uniformTypeIdentifier())
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(video_resource.uniformTypeIdentifier())
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# exported = []
|
||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||
# causes pathlib.Path().is_file() to fail in tests
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
)
|
||||
|
||||
# if photo:
|
||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# photo_resource, photo_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(photo_output_file))
|
||||
photo_ext = get_preferred_uti_extension(
|
||||
photo_resource.uniformTypeIdentifier()
|
||||
)
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(
|
||||
video_resource.uniformTypeIdentifier()
|
||||
)
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
|
||||
# if video:
|
||||
# video_output_url = path_to_NSURL(video_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# video_resource, video_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(video_output_file))
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
|
||||
# def completion_handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# exported = []
|
||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||
# causes pathlib.Path().is_file() to fail in tests
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
# if photo:
|
||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# photo_resource, photo_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(photo_output_file))
|
||||
|
||||
return exported
|
||||
# if video:
|
||||
# video_output_url = path_to_NSURL(video_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# video_resource, video_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(video_output_file))
|
||||
|
||||
# def completion_handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
del data
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
del data
|
||||
|
||||
request.dealloc()
|
||||
return exported
|
||||
|
||||
def _request_resource_data(self, resource):
|
||||
""" Request asset resource data (either photo or video component)
|
||||
@@ -1015,33 +1070,40 @@ class LivePhotoAsset(PhotoAsset):
|
||||
Raises:
|
||||
"""
|
||||
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
with objc.autorelease_pool():
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
|
||||
requestdata = PHAssetResourceData()
|
||||
event = threading.Event()
|
||||
requestdata = PHAssetResourceData()
|
||||
event = threading.Event()
|
||||
|
||||
def handler(data):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
def handler(data):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
nonlocal requestdata
|
||||
nonlocal requestdata
|
||||
|
||||
requestdata.data += data
|
||||
requestdata.data += data
|
||||
|
||||
def completion_handler(error):
|
||||
if error:
|
||||
raise PhotoKitExportError("Error requesting data for asset resource")
|
||||
event.set()
|
||||
def completion_handler(error):
|
||||
if error:
|
||||
raise PhotoKitExportError(
|
||||
"Error requesting data for asset resource"
|
||||
)
|
||||
event.set()
|
||||
|
||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||
resource, options, handler, completion_handler
|
||||
)
|
||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||
resource, options, handler, completion_handler
|
||||
)
|
||||
|
||||
event.wait()
|
||||
options.dealloc()
|
||||
return requestdata.data
|
||||
event.wait()
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata.data)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
|
||||
# # Returns an NSImage which isn't overly useful
|
||||
@@ -1127,19 +1189,20 @@ class PhotoLibrary:
|
||||
"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||
uuid_list, fetch_options
|
||||
)
|
||||
if fetch_result and fetch_result.count() >= 1:
|
||||
return [
|
||||
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
||||
for idx in range(fetch_result.count())
|
||||
]
|
||||
else:
|
||||
raise PhotoKitFetchFailed(
|
||||
f"Fetch did not return result for uuid_list {uuid_list}"
|
||||
with objc.autorelease_pool():
|
||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||
uuid_list, fetch_options
|
||||
)
|
||||
if fetch_result and fetch_result.count() >= 1:
|
||||
return [
|
||||
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
||||
for idx in range(fetch_result.count())
|
||||
]
|
||||
else:
|
||||
raise PhotoKitFetchFailed(
|
||||
f"Fetch did not return result for uuid_list {uuid_list}"
|
||||
)
|
||||
|
||||
def fetch_uuid(self, uuid):
|
||||
""" fetch PHAsset with uuid = uuid
|
||||
|
||||
@@ -70,12 +70,13 @@ class PhotosDB:
|
||||
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
||||
from ._photosdb_process_comments import _process_comments
|
||||
|
||||
def __init__(self, dbfile=None, verbose=None):
|
||||
def __init__(self, dbfile=None, verbose=None, exiftool=None):
|
||||
""" Create a new PhotosDB object.
|
||||
|
||||
Args:
|
||||
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
|
||||
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||
exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if dbfile is not a valid Photos library.
|
||||
@@ -84,12 +85,12 @@ class PhotosDB:
|
||||
|
||||
# Check OS version
|
||||
system = platform.system()
|
||||
(_, major, _) = _get_os_version()
|
||||
if system != "Darwin" or (major not in _TESTED_OS_VERSIONS):
|
||||
(ver, major, _) = _get_os_version()
|
||||
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
|
||||
logging.warning(
|
||||
f"WARNING: This module has only been tested with MacOS 10."
|
||||
f"[{', '.join(_TESTED_OS_VERSIONS)}]: "
|
||||
f"you have {system}, OS version: {major}"
|
||||
f"WARNING: This module has only been tested with macOS versions "
|
||||
f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: "
|
||||
f"you have {system}, OS version: {ver}.{major}"
|
||||
)
|
||||
|
||||
if verbose is None:
|
||||
@@ -98,6 +99,11 @@ class PhotosDB:
|
||||
raise TypeError("verbose must be callable")
|
||||
self._verbose = verbose
|
||||
|
||||
# enable beta features
|
||||
self._beta = False
|
||||
|
||||
self._exiftool_path = exiftool
|
||||
|
||||
# create a temporary directory
|
||||
# tempfile.TemporaryDirectory gets cleaned up when the object does
|
||||
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
@@ -884,7 +890,10 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.fileSize,
|
||||
RKVersion.subType
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -912,8 +921,11 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.originalFileSize,
|
||||
RKVersion.subType
|
||||
FROM RKVersion, RKMaster
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
|
||||
@@ -959,6 +971,9 @@ class PhotosDB:
|
||||
# 38 RKMaster.orientation,
|
||||
# 39 RKMaster.originalFileSize
|
||||
# 40 RKVersion.subType
|
||||
# 41 RKVersion.inTrashDate
|
||||
# 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)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1133,7 +1148,14 @@ class PhotosDB:
|
||||
)
|
||||
|
||||
# recently deleted items
|
||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||
self._dbphotos[uuid]["intrash"] = row[32] == 1
|
||||
self._dbphotos[uuid]["trasheddate_timestamp"] = row[41]
|
||||
try:
|
||||
self._dbphotos[uuid]["trasheddate"] = datetime.fromtimestamp(
|
||||
row[41] + TIME_DELTA
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
self._dbphotos[uuid]["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
self._dbphotos[uuid]["height"] = row[33]
|
||||
@@ -1144,6 +1166,14 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||
|
||||
# visibility state
|
||||
self._dbphotos[uuid]["visibility_state"] = row[42]
|
||||
self._dbphotos[uuid]["visible"] = row[42] == 1
|
||||
|
||||
# file is reference (not copied into Photos library)
|
||||
self._dbphotos[uuid]["isreference"] = row[43] == 1
|
||||
self._dbphotos[uuid]["saved_asset_type"] = None # Photos 5+
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
@@ -1837,7 +1867,10 @@ class PhotosDB:
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
|
||||
{depth_state},
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP,
|
||||
{asset_table}.ZVISIBILITYSTATE,
|
||||
{asset_table}.ZTRASHEDDATE,
|
||||
{asset_table}.ZSAVEDASSETTYPE
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1882,6 +1915,9 @@ class PhotosDB:
|
||||
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
|
||||
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
|
||||
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
|
||||
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
|
||||
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1898,9 +1934,7 @@ class PhotosDB:
|
||||
info["lastmodifieddate_timestamp"] = row[37]
|
||||
try:
|
||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
|
||||
except ValueError:
|
||||
info["lastmodifieddate"] = None
|
||||
except TypeError:
|
||||
except (ValueError, TypeError):
|
||||
info["lastmodifieddate"] = None
|
||||
|
||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||
@@ -2043,6 +2077,11 @@ class PhotosDB:
|
||||
|
||||
# recently deleted items
|
||||
info["intrash"] = True if row[28] == 1 else False
|
||||
info["trasheddate_timestamp"] = row[39]
|
||||
try:
|
||||
info["trasheddate"] = datetime.fromtimestamp(row[39] + TIME_DELTA)
|
||||
except (ValueError, TypeError):
|
||||
info["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
info["height"] = row[29]
|
||||
@@ -2053,6 +2092,19 @@ class PhotosDB:
|
||||
info["original_orientation"] = row[34]
|
||||
info["original_filesize"] = row[35]
|
||||
|
||||
# visibility state, visible (True) if 0, otherwise not visible (False)
|
||||
# only values I've seen are 0 for visible, 2 for not-visible
|
||||
info["visibility_state"] = row[38]
|
||||
info["visible"] = row[38] == 0
|
||||
|
||||
# ZSAVEDASSETTYPE Values:
|
||||
# 3: imported by copying to Photos library
|
||||
# 4: shared iCloud photo
|
||||
# 6: imported by iCloud (e.g. from iPhone)
|
||||
# 10: referenced file (not copied to Photos library)
|
||||
info["saved_asset_type"] = row[40]
|
||||
info["isreference"] = row[40] == 10
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
# 2. Needed to handle default values if template not found
|
||||
# 3. Didn't want user to need to know python (e.g. by using Mako which is
|
||||
# already used elsewhere in this project)
|
||||
# 4. Couldn't figure out how to do #1 and #2 with str.format()
|
||||
#
|
||||
# This code isn't elegant but it seems to work well. PRs gladly accepted.
|
||||
# This code isn't elegant and is prime for refactoring but it seems to work well. PRs gladly accepted.
|
||||
|
||||
import datetime
|
||||
import locale
|
||||
import os
|
||||
@@ -70,23 +70,23 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
+ "If used with no template will return null value. "
|
||||
+ "See https://strftime.org/ for help on strftime templates.",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||
"{modified.year}": "4-digit year of photo modification time",
|
||||
"{modified.yy}": "2-digit year of photo modification time",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded)",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time",
|
||||
"{modified.min}": "2-digit minute of the photo modification time",
|
||||
"{modified.sec}": "2-digit second of the photo modification time",
|
||||
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
# + "If used with no template will return null value. "
|
||||
# + "See https://strftime.org/ for help on strftime templates.",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified",
|
||||
"{modified.year}": "4-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.yy}": "2-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.min}": "2-digit minute of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.sec}": "2-digit second of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||
+ "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||
+ "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
+ "If used with no template will return null value. Uses creation date if photo is not modified. "
|
||||
+ "See https://strftime.org/ for help on strftime templates.",
|
||||
"{today.date}": "Current date in iso format, e.g. '2020-03-22'",
|
||||
"{today.year}": "4-digit year of current date",
|
||||
"{today.yy}": "2-digit year of current date",
|
||||
@@ -117,6 +117,11 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{place.address.postal_code}": "Postal code part of the postal address, e.g. '20009'",
|
||||
"{place.address.country}": "Country name of the postal address, e.g. 'United States'",
|
||||
"{place.address.country_code}": "ISO country code of the postal address, e.g. 'US'",
|
||||
"{searchinfo.season}": "Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{exif.camera_make}": "Camera make from original photo's EXIF information as imported by Photos, e.g. 'Apple'",
|
||||
"{exif.camera_model}": "Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'",
|
||||
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
|
||||
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
||||
}
|
||||
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
@@ -125,13 +130,17 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{keyword}": "Keyword(s) assigned to photo",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
||||
"{exiftool:GROUP:TAGNAME}": "Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
|
||||
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
|
||||
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
|
||||
"{searchinfo.holiday}": "Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
}
|
||||
|
||||
# Just the multi-valued substitution names without the braces
|
||||
@@ -140,25 +149,48 @@ MULTI_VALUE_SUBSTITUTIONS = [
|
||||
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||
]
|
||||
|
||||
# regular expressions for matching template syntax
|
||||
RE_OPENING_BRACE = r"(?<!\{)\{" # match { but not {{
|
||||
RE_DELIM = r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
RE_FIELD_NAME = r"([^\\,}+\?]+)" # group 2: field name
|
||||
RE_PATH_SEP = r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
# + r"(\[[^{}\)]*\])?" # group 4: optional [REPLACE]
|
||||
RE_REPLACE = r"(\[[^{}]*\])?" # group 4: optional [REPLACE]
|
||||
RE_BOOL_VAL = r"(\?[^\\,}]*)?" # group 5: optional ?TRUE_VALUE for boolean fields
|
||||
RE_DEFAULT_VAL = r"(,[\w\=\;\-\%. ]*)?" # group 6: optional ,DEFAULT
|
||||
RE_CLOSING_BRACE = r"(?=\}(?!\}))\}" # match } but not }}
|
||||
|
||||
MATCH_GROUPS_TOTAL = 6
|
||||
MATCH_GROUPS_DELIM = 1
|
||||
MATCH_GROUPS_FIELD = 2
|
||||
MATCH_GROUPS_PATH_SEP = 3
|
||||
MATCH_GROUPS_REPLACE = 4
|
||||
MATCH_GROUPS_BOOL_VAL = 5
|
||||
MATCH_GROUPS_DEFAULT = 6
|
||||
|
||||
# default values for string manipulation template options
|
||||
INPLACE_DEFAULT = ","
|
||||
PATH_SEP_DEFAULT = os.path.sep
|
||||
|
||||
|
||||
class PhotoTemplate:
|
||||
""" PhotoTemplate class to render a template string from a PhotoInfo object """
|
||||
|
||||
def __init__(self, photo):
|
||||
""" Inits PhotoTemplate class with photo, non_str, and path_sep
|
||||
def __init__(self, photo, exiftool_path=None):
|
||||
""" Inits PhotoTemplate class with photo
|
||||
|
||||
Args:
|
||||
photo: a PhotoInfo instance.
|
||||
exiftool_path: optional path to exiftool for use with {exiftool:} template; if not provided, will look for exiftool in $PATH
|
||||
"""
|
||||
self.photo = photo
|
||||
self.exiftool_path = exiftool_path
|
||||
|
||||
# holds value of current date/time for {today.x} fields
|
||||
# gets initialized in get_template_value
|
||||
self.today = None
|
||||
|
||||
def make_subst_function(
|
||||
self, none_str, filename, dirname, replacement, get_func=None
|
||||
):
|
||||
def make_subst_function(self, none_str, filename, dirname, get_func=None):
|
||||
""" returns: substitution function for use in re.sub
|
||||
none_str: value to use if substitution lookup is None and no default provided
|
||||
get_func: function that gets the substitution value for a given template field
|
||||
@@ -167,37 +199,39 @@ class PhotoTemplate:
|
||||
if get_func is None:
|
||||
# used by make_subst_function to get the value for a template substitution
|
||||
get_func = partial(
|
||||
self.get_template_value,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
self.get_template_value, filename=filename, dirname=dirname
|
||||
)
|
||||
|
||||
# closure to capture photo, none_str, filename, dirname in subst
|
||||
def subst(matchobj):
|
||||
groups = len(matchobj.groups())
|
||||
if groups != 5:
|
||||
if groups != MATCH_GROUPS_TOTAL:
|
||||
raise ValueError(
|
||||
f"Unexpected number of groups: expected 4, got {groups}"
|
||||
f"Unexpected number of groups: expected {MATCH_GROUPS_TOTAL}, got {groups}"
|
||||
)
|
||||
|
||||
delim = matchobj.group(1)
|
||||
field = matchobj.group(2)
|
||||
path_sep = matchobj.group(3)
|
||||
bool_val = matchobj.group(4)
|
||||
default = matchobj.group(5)
|
||||
delim = matchobj.group(MATCH_GROUPS_DELIM)
|
||||
field = matchobj.group(MATCH_GROUPS_FIELD)
|
||||
path_sep = matchobj.group(MATCH_GROUPS_PATH_SEP)
|
||||
replace = matchobj.group(MATCH_GROUPS_REPLACE)
|
||||
bool_val = matchobj.group(MATCH_GROUPS_BOOL_VAL)
|
||||
default = matchobj.group(MATCH_GROUPS_DEFAULT)
|
||||
|
||||
# drop the '+' on delim
|
||||
delim = delim[:-1] if delim is not None else None
|
||||
# drop () from path_sep
|
||||
path_sep = path_sep.strip("()") if path_sep is not None else None
|
||||
# drop [] from replace
|
||||
replace = replace[1:-1] if replace is not None else None
|
||||
# drop the ? on bool_val
|
||||
bool_val = bool_val[1:] if bool_val is not None else None
|
||||
# drop the comma on default
|
||||
default_val = default[1:] if default is not None else None
|
||||
|
||||
try:
|
||||
val = get_func(field, default_val, bool_val, delim, path_sep)
|
||||
val = get_func(
|
||||
field, default_val, bool_val, delim, path_sep, replacement=replace
|
||||
)
|
||||
except ValueError:
|
||||
return matchobj.group(0)
|
||||
|
||||
@@ -221,7 +255,7 @@ class PhotoTemplate:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
strip=False,
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
@@ -235,17 +269,17 @@ class PhotoTemplate:
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||
strip: if True, strips leading/trailing whitespace from rendered templates
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
"""
|
||||
|
||||
if path_sep is None:
|
||||
path_sep = os.path.sep
|
||||
path_sep = PATH_SEP_DEFAULT
|
||||
|
||||
if inplace_sep is None:
|
||||
inplace_sep = ","
|
||||
inplace_sep = INPLACE_DEFAULT
|
||||
|
||||
# the rendering happens in two phases:
|
||||
# phase 1: handle all the single-value template substitutions
|
||||
@@ -258,19 +292,20 @@ class PhotoTemplate:
|
||||
# regex to find {template_field,optional_default} in strings
|
||||
# pylint: disable=anomalous-backslash-in-string
|
||||
regex = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
+ r"([^\\,}+\?]+)" # group 2: field name
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ RE_FIELD_NAME
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
|
||||
if type(template) is not str:
|
||||
raise TypeError(f"template must be type str, not {type(template)}")
|
||||
|
||||
subst_func = self.make_subst_function(none_str, filename, dirname, replacement)
|
||||
subst_func = self.make_subst_function(none_str, filename, dirname)
|
||||
|
||||
# do the replacements
|
||||
rendered = re.sub(regex, subst_func, template)
|
||||
@@ -299,14 +334,7 @@ class PhotoTemplate:
|
||||
# '2011/Album2/keyword2/person1',]
|
||||
|
||||
rendered_strings = self._render_multi_valued_templates(
|
||||
rendered,
|
||||
none_str,
|
||||
path_sep,
|
||||
expand_inplace,
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
rendered, none_str, path_sep, expand_inplace, inplace_sep, filename, dirname
|
||||
)
|
||||
|
||||
# process exiftool: templates
|
||||
@@ -318,7 +346,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
)
|
||||
|
||||
# find any {fields} that weren't replaced
|
||||
@@ -343,6 +370,11 @@ class PhotoTemplate:
|
||||
sanitize_filename(rendered_str) for rendered_str in rendered_strings
|
||||
]
|
||||
|
||||
if strip:
|
||||
rendered_strings = [
|
||||
rendered_str.strip() for rendered_str in rendered_strings
|
||||
]
|
||||
|
||||
return rendered_strings, unmatched
|
||||
|
||||
def _render_multi_valued_templates(
|
||||
@@ -354,7 +386,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
):
|
||||
rendered_strings = [rendered]
|
||||
new_rendered_strings = []
|
||||
@@ -363,15 +394,16 @@ class PhotoTemplate:
|
||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||
# Build a regex that matches only the field being processed
|
||||
re_str = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ r"("
|
||||
+ field # group 2: field name
|
||||
+ r")"
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
regex_multi = re.compile(re_str)
|
||||
|
||||
@@ -382,21 +414,29 @@ class PhotoTemplate:
|
||||
matches = regex_multi.search(str_template)
|
||||
if matches:
|
||||
path_sep = (
|
||||
matches.group(3).strip("()")
|
||||
if matches.group(3) is not None
|
||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
||||
else path_sep
|
||||
)
|
||||
replace = (
|
||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
||||
else None
|
||||
)
|
||||
values = self.get_template_value_multi(
|
||||
field,
|
||||
path_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
replacement=replace,
|
||||
)
|
||||
if expand_inplace or matches.group(1) is not None:
|
||||
if (
|
||||
expand_inplace
|
||||
or matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
):
|
||||
delim = (
|
||||
matches.group(1)[:-1]
|
||||
if matches.group(1) is not None
|
||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
else inplace_sep
|
||||
)
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
@@ -406,7 +446,9 @@ class PhotoTemplate:
|
||||
else None
|
||||
)
|
||||
|
||||
def lookup_template_value_multi(lookup_value, *_):
|
||||
def lookup_template_value_multi(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -422,7 +464,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_multi,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -433,7 +474,9 @@ class PhotoTemplate:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_multi(lookup_value, *_):
|
||||
def lookup_template_value_multi(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -449,7 +492,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_multi,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -468,7 +510,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
):
|
||||
# TODO: lots of code commonality with render_multi_valued_templates -- combine or pull out
|
||||
# TODO: put these in globals
|
||||
@@ -479,15 +520,15 @@ class PhotoTemplate:
|
||||
inplace_sep = ","
|
||||
|
||||
# Build a regex that matches only the field being processed
|
||||
# todo: pull out regexes into globals?
|
||||
re_str = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
+ r"(exiftool:[^\\,}+\?]+)" # group 3 field name
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ r"(exiftool:[^\\,}+\?\[\]]+)" # group 3 field name
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
regex_multi = re.compile(re_str)
|
||||
|
||||
@@ -502,17 +543,21 @@ class PhotoTemplate:
|
||||
# allmatches = regex_multi.finditer(str_template)
|
||||
# for matches in allmatches:
|
||||
path_sep = (
|
||||
matches.group(3).strip("()")
|
||||
if matches.group(3) is not None
|
||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
||||
else path_sep
|
||||
)
|
||||
field = matches.group(2)
|
||||
replace = (
|
||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
||||
else None
|
||||
)
|
||||
field = matches.group(MATCH_GROUPS_FIELD)
|
||||
subfield = field[9:]
|
||||
|
||||
if not self.photo.path:
|
||||
values = [None]
|
||||
else:
|
||||
exif = ExifTool(self.photo.path)
|
||||
exif = ExifTool(self.photo.path, exiftool=self.exiftool_path)
|
||||
exifdict = exif.asdict()
|
||||
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||
subfield = subfield.lower()
|
||||
@@ -521,12 +566,24 @@ class PhotoTemplate:
|
||||
values = (
|
||||
[values] if not isinstance(values, list) else values
|
||||
)
|
||||
if replace and values:
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.append(self.replace(value, replace))
|
||||
values = new_values
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname:
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
else:
|
||||
values = [None]
|
||||
if expand_inplace or matches.group(1) is not None:
|
||||
if expand_inplace or matches.group(MATCH_GROUPS_DELIM) is not None:
|
||||
delim = (
|
||||
matches.group(1)[:-1]
|
||||
if matches.group(1) is not None
|
||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
else inplace_sep
|
||||
)
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
@@ -534,7 +591,7 @@ class PhotoTemplate:
|
||||
delim.join(sorted(values)) if values and values[0] else None
|
||||
)
|
||||
|
||||
def lookup_template_value_exif(lookup_value, *_):
|
||||
def lookup_template_value_exif(lookup_value, *args, **kwargs):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -548,7 +605,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_exif,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -558,7 +614,9 @@ class PhotoTemplate:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_exif(lookup_value, *_):
|
||||
def lookup_template_value_exif(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -574,7 +632,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_exif,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -592,7 +649,7 @@ class PhotoTemplate:
|
||||
path_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
replacement=None,
|
||||
):
|
||||
"""lookup value for template field (single-value template substitutions)
|
||||
|
||||
@@ -672,74 +729,83 @@ class PhotoTemplate:
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).date
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).date
|
||||
)
|
||||
elif field == "modified.year":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).year
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).year
|
||||
)
|
||||
elif field == "modified.yy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).yy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).yy
|
||||
)
|
||||
elif field == "modified.mm":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mm
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mm
|
||||
)
|
||||
elif field == "modified.month":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).month
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).month
|
||||
)
|
||||
elif field == "modified.mon":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mon
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mon
|
||||
)
|
||||
elif field == "modified.dd":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dd
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dd
|
||||
)
|
||||
elif field == "modified.dow":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dow
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dow
|
||||
)
|
||||
elif field == "modified.doy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).doy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).doy
|
||||
)
|
||||
elif field == "modified.hour":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).hour
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).hour
|
||||
)
|
||||
elif field == "modified.min":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).min
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).min
|
||||
)
|
||||
elif field == "modified.sec":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).sec
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).sec
|
||||
)
|
||||
elif field == "modified.strftime":
|
||||
if default:
|
||||
try:
|
||||
date = self.photo.date_modified or self.photo.date
|
||||
value = date.strftime(default)
|
||||
except:
|
||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||
else:
|
||||
value = None
|
||||
elif field == "today.date":
|
||||
value = DateTimeFormatter(self.today).date
|
||||
elif field == "today.year":
|
||||
@@ -842,18 +908,58 @@ class PhotoTemplate:
|
||||
if self.photo.place and self.photo.place.address.iso_country_code
|
||||
else None
|
||||
)
|
||||
elif field == "searchinfo.season":
|
||||
value = self.photo.search_info.season if self.photo.search_info else None
|
||||
elif field == "exif.camera_make":
|
||||
value = self.photo.exif_info.camera_make if self.photo.exif_info else None
|
||||
elif field == "exif.camera_model":
|
||||
value = self.photo.exif_info.camera_model if self.photo.exif_info else None
|
||||
elif field == "exif.lens_model":
|
||||
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
|
||||
elif field == "uuid":
|
||||
value = self.photo.uuid
|
||||
else:
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
if value and replacement:
|
||||
value = self.replace(value, replacement)
|
||||
# process character replacements
|
||||
|
||||
if filename:
|
||||
value = sanitize_pathpart(value, replacement=replacement)
|
||||
value = sanitize_pathpart(value)
|
||||
elif dirname:
|
||||
value = sanitize_dirname(value, replacement=replacement)
|
||||
value = sanitize_dirname(value)
|
||||
|
||||
return value
|
||||
|
||||
def replace(self, value, replacement):
|
||||
""" process REPLACE template option
|
||||
|
||||
Args:
|
||||
value: str value to process
|
||||
replacement: str in form OLD,NEW|OLD,NEW... with old and new values for replacement
|
||||
|
||||
Returns:
|
||||
value with all replacements done
|
||||
|
||||
Raises:
|
||||
ValueError if replacement string is in wrong format
|
||||
"""
|
||||
if not value:
|
||||
return value
|
||||
|
||||
replacements = replacement.split("|")
|
||||
for r in replacements:
|
||||
try:
|
||||
old, new = r.split(",")
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid template REPLACE value: {replacement}")
|
||||
value = value.replace(old, new)
|
||||
return value
|
||||
|
||||
def get_template_value_multi(
|
||||
self, field, path_sep, filename=False, dirname=False, replacement=":"
|
||||
self, field, path_sep, filename=False, dirname=False, replacement=None
|
||||
):
|
||||
"""lookup value for template field (multi-value template substitutions)
|
||||
|
||||
@@ -892,12 +998,9 @@ class PhotoTemplate:
|
||||
if dirname:
|
||||
# being used as a filepath so sanitize each part
|
||||
folder = path_sep.join(
|
||||
sanitize_dirname(f, replacement=replacement)
|
||||
for f in album.folder_names
|
||||
)
|
||||
folder += path_sep + sanitize_dirname(
|
||||
album.title, replacement=replacement
|
||||
sanitize_dirname(f) for f in album.folder_names
|
||||
)
|
||||
folder += path_sep + sanitize_dirname(album.title)
|
||||
else:
|
||||
folder = path_sep.join(album.folder_names)
|
||||
folder += path_sep + album.title
|
||||
@@ -905,28 +1008,41 @@ class PhotoTemplate:
|
||||
else:
|
||||
# album not in folder
|
||||
if dirname:
|
||||
values.append(
|
||||
sanitize_dirname(album.title, replacement=replacement)
|
||||
)
|
||||
values.append(sanitize_dirname(album.title))
|
||||
else:
|
||||
values.append(album.title)
|
||||
elif field == "comment":
|
||||
values = [
|
||||
f"{comment.user}: {comment.text}" for comment in self.photo.comments
|
||||
]
|
||||
elif field == "searchinfo.holiday":
|
||||
values = self.photo.search_info.holidays if self.photo.search_info else []
|
||||
elif field == "searchinfo.activity":
|
||||
values = self.photo.search_info.activities if self.photo.search_info else []
|
||||
elif field == "searchinfo.venue":
|
||||
values = self.photo.search_info.venues if self.photo.search_info else []
|
||||
elif field == "searchinfo.venue_type":
|
||||
values = (
|
||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||
)
|
||||
elif not field.startswith("exiftool:"):
|
||||
# exiftool: templates handled by _render_exiftool_template
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
# do any replacements needs
|
||||
if replacement:
|
||||
new_values = []
|
||||
for value in values:
|
||||
# process replacements
|
||||
new_values.append(self.replace(value, replacement))
|
||||
values = new_values
|
||||
|
||||
# sanitize directory names if needed, folder_album handled differently above
|
||||
if filename:
|
||||
values = [
|
||||
sanitize_pathpart(value, replacement=replacement) for value in values
|
||||
]
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname and field != "folder_album":
|
||||
# skip folder_album because it would have been handled above
|
||||
values = [
|
||||
sanitize_dirname(value, replacement=replacement) for value in values
|
||||
]
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
# If no values, insert None so code below will substite none_str for None
|
||||
values = values or [None]
|
||||
|
||||
8
osxphotos/templates/DESCRIPTION.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Templates for creating XMP sidecar files
|
||||
|
||||
* xmp_sidecar.mako: Mako template for generating XMP sidecar
|
||||
* xmp_sidecar_beta.mako: template for beta testing new XMP properties, if run with --beta, osxphotos will use this template, otherwise it always uses xmp_sidecar.mako
|
||||
|
||||
Reference:
|
||||
* https://www.adobe.com/devnet/xmp.html
|
||||
* https://www.makotemplates.org/
|
||||
@@ -1,5 +1,3 @@
|
||||
<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
|
||||
|
||||
<%def name="photoshop_sidecar_for_extension(extension)">
|
||||
% if extension is None:
|
||||
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
|
||||
@@ -10,29 +8,44 @@
|
||||
|
||||
<%def name="dc_description(desc)">
|
||||
% if desc is None:
|
||||
<dc:description></dc:description>
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% else:
|
||||
<dc:description>${desc | x}</dc:description>
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${desc | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_title(title)">
|
||||
% if title is None:
|
||||
<dc:title></dc:title>
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% else:
|
||||
<dc:title>${title | x}</dc:title>
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${title | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_subject(subject)">
|
||||
% if subject:
|
||||
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||
<dc:subject>
|
||||
<rdf:Seq>
|
||||
% for subj in subject:
|
||||
<rdf:li>${subj | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Seq>
|
||||
<rdf:Bag>
|
||||
% for subj in subject:
|
||||
<rdf:li>${subj | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
% endif
|
||||
</%def>
|
||||
@@ -46,11 +59,11 @@
|
||||
<%def name="iptc_personinimage(persons)">
|
||||
% if persons:
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
% for person in persons:
|
||||
<rdf:li>${person | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
<rdf:Bag>
|
||||
% for person in persons:
|
||||
<rdf:li>${person | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
% endif
|
||||
</%def>
|
||||
@@ -58,11 +71,11 @@
|
||||
<%def name="dk_tagslist(keywords)">
|
||||
% if keywords:
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
% for keyword in keywords:
|
||||
<rdf:li>${keyword | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Seq>
|
||||
<rdf:Seq>
|
||||
% for keyword in keywords:
|
||||
<rdf:li>${keyword | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
% endif
|
||||
</%def>
|
||||
@@ -86,34 +99,112 @@
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
||||
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
${photoshop_sidecar_for_extension(extension)}
|
||||
${dc_description(description)}
|
||||
${dc_title(photo.title)}
|
||||
${dc_subject(subjects)}
|
||||
${dc_datecreated(photo.date)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
${iptc_personinimage(persons)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
${dk_tagslist(keywords)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
${adobe_createdate(photo.date)}
|
||||
${adobe_modifydate(photo.date)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
${gps_info(*photo.location)}
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<%def name="orientation(orientation)">
|
||||
% if orientation is not None:
|
||||
<tiff:Orientation>${orientation}</tiff:Orientation>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mwg_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<mwg-rs:Regions rdf:parseType="Resource">
|
||||
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
|
||||
<stDim:unit>pixel</stDim:unit>
|
||||
</mwg-rs:AppliedToDimensions>
|
||||
<mwg-rs:RegionList>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<mwg-rs:Area rdf:parseType="Resource">
|
||||
<stArea:h>${'{0:.6f}'.format(face.mwg_rs_area.h)}</stArea:h>
|
||||
<stArea:w>${'{0:.6f}'.format(face.mwg_rs_area.w)}</stArea:w>
|
||||
<stArea:x>${'{0:.6f}'.format(face.mwg_rs_area.x)}</stArea:x>
|
||||
<stArea:y>${'{0:.6f}'.format(face.mwg_rs_area.y)}</stArea:y>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>${face.name}</mwg-rs:Name>
|
||||
<mwg-rs:Rotation>${face.roll}</mwg-rs:Rotation>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</mwg-rs:RegionList>
|
||||
</mwg-rs:Regions>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mpri_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<MP:RegionInfo rdf:parseType="Resource">
|
||||
<MPRI:Regions>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<MPReg:PersonDisplayName>${face.name}</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>${'{0:.6f}'.format(face.mpri_reg_rect.x)}, ${'{0:.6f}'.format(face.mpri_reg_rect.y)}, ${'{0:.6f}'.format(face.mpri_reg_rect.h)}, ${'{0:.6f}'.format(face.mpri_reg_rect.w)}</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</MPRI:Regions>
|
||||
</MP:RegionInfo>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
${photoshop_sidecar_for_extension(extension)}
|
||||
${dc_description(description)}
|
||||
${dc_title(photo.title)}
|
||||
${dc_subject(subjects)}
|
||||
${dc_datecreated(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
${iptc_personinimage(persons)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
${dk_tagslist(keywords)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
${adobe_createdate(photo.date)}
|
||||
${adobe_modifydate(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
${gps_info(*photo.location)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||
${orientation(photo.orientation)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
|
||||
${mwg_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:MP="http://ns.microsoft.com/photo/1.2/"
|
||||
xmlns:MPRI="http://ns.microsoft.com/photo/1.2/t/RegionInfo#"
|
||||
xmlns:MPReg="http://ns.microsoft.com/photo/1.2/t/Region#">
|
||||
${mpri_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end="w"?>
|
||||
210
osxphotos/templates/xmp_sidecar_beta.mako
Normal file
@@ -0,0 +1,210 @@
|
||||
<%def name="photoshop_sidecar_for_extension(extension)">
|
||||
% if extension is None:
|
||||
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
|
||||
% else:
|
||||
<photoshop:SidecarForExtension>${extension}</photoshop:SidecarForExtension>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_description(desc)">
|
||||
% if desc is None:
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% else:
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${desc | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_title(title)">
|
||||
% if title is None:
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% else:
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${title | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_subject(subject)">
|
||||
% if subject:
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
% for subj in subject:
|
||||
<rdf:li>${subj | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_datecreated(date)">
|
||||
% if date is not None:
|
||||
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="iptc_personinimage(persons)">
|
||||
% if persons:
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
% for person in persons:
|
||||
<rdf:li>${person | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dk_tagslist(keywords)">
|
||||
% if keywords:
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
% for keyword in keywords:
|
||||
<rdf:li>${keyword | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="adobe_createdate(date)">
|
||||
% if date is not None:
|
||||
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="adobe_modifydate(date)">
|
||||
% if date is not None:
|
||||
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="gps_info(latitude, longitude)">
|
||||
% if latitude is not None and longitude is not None:
|
||||
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
|
||||
<exif:GPSLatitude>${int(abs(latitude))},${(abs(latitude) % 1) * 60}${"N" if latitude >= 0 else "S"}</exif:GPSLatitude>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="orientation(orientation)">
|
||||
% if orientation is not None:
|
||||
<tiff:Orientation>${orientation}</tiff:Orientation>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mwg_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<mwg-rs:Regions rdf:parseType="Resource">
|
||||
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
|
||||
<stDim:unit>pixel</stDim:unit>
|
||||
</mwg-rs:AppliedToDimensions>
|
||||
<mwg-rs:RegionList>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<mwg-rs:Area rdf:parseType="Resource">
|
||||
<stArea:h>${'{0:.6f}'.format(face.mwg_rs_area.h)}</stArea:h>
|
||||
<stArea:w>${'{0:.6f}'.format(face.mwg_rs_area.w)}</stArea:w>
|
||||
<stArea:x>${'{0:.6f}'.format(face.mwg_rs_area.x)}</stArea:x>
|
||||
<stArea:y>${'{0:.6f}'.format(face.mwg_rs_area.y)}</stArea:y>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>${face.name}</mwg-rs:Name>
|
||||
<mwg-rs:Rotation>${face.roll}</mwg-rs:Rotation>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</mwg-rs:RegionList>
|
||||
</mwg-rs:Regions>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mpri_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<MP:RegionInfo rdf:parseType="Resource">
|
||||
<MPRI:Regions>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<MPReg:PersonDisplayName>${face.name}</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>${'{0:.6f}'.format(face.mpri_reg_rect.x)}, ${'{0:.6f}'.format(face.mpri_reg_rect.y)}, ${'{0:.6f}'.format(face.mpri_reg_rect.h)}, ${'{0:.6f}'.format(face.mpri_reg_rect.w)}</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</MPRI:Regions>
|
||||
</MP:RegionInfo>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
${photoshop_sidecar_for_extension(extension)}
|
||||
${dc_description(description)}
|
||||
${dc_title(photo.title)}
|
||||
${dc_subject(subjects)}
|
||||
${dc_datecreated(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
${iptc_personinimage(persons)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
${dk_tagslist(keywords)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
${adobe_createdate(photo.date)}
|
||||
${adobe_modifydate(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
${gps_info(*photo.location)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||
${orientation(photo.orientation)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
|
||||
${mwg_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:MP="http://ns.microsoft.com/photo/1.2/"
|
||||
xmlns:MPRI="http://ns.microsoft.com/photo/1.2/t/RegionInfo#"
|
||||
xmlns:MPReg="http://ns.microsoft.com/photo/1.2/t/Region#">
|
||||
${mpri_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end="w"?>
|
||||
@@ -1,5 +1,6 @@
|
||||
import fnmatch
|
||||
import glob
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
@@ -9,7 +10,6 @@ import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
from plistlib import load as plistload
|
||||
@@ -19,7 +19,6 @@ import CoreServices
|
||||
import objc
|
||||
|
||||
from ._constants import UNICODE_FORMAT
|
||||
from .fileutil import FileUtil
|
||||
|
||||
_DEBUG = False
|
||||
|
||||
@@ -62,9 +61,17 @@ def noop(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def lineno(filename):
|
||||
""" Returns string with filename and current line number in caller as '(filename): line_num'
|
||||
Will trim filename to just the name, dropping path, if any. """
|
||||
line = inspect.currentframe().f_back.f_lineno
|
||||
filename = pathlib.Path(filename).name
|
||||
return f"{filename}: {line}"
|
||||
|
||||
|
||||
def _get_os_version():
|
||||
# returns tuple containing OS version
|
||||
# e.g. 10.13.6 = (10, 13, 6)
|
||||
# returns tuple of str containing OS version
|
||||
# e.g. 10.13.6 = ("10", "13", "6")
|
||||
version = platform.mac_ver()[0].split(".")
|
||||
if len(version) == 2:
|
||||
(ver, major) = version
|
||||
@@ -260,10 +267,10 @@ def get_preferred_uti_extension(uti):
|
||||
returns: preferred extension as str """
|
||||
|
||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||
|
||||
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||
uti, CoreServices.kUTTagClassFilenameExtension
|
||||
)
|
||||
with objc.autorelease_pool():
|
||||
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||
uti, CoreServices.kUTTagClassFilenameExtension
|
||||
)
|
||||
|
||||
|
||||
def findfiles(pattern, path_):
|
||||
|
||||
@@ -9,8 +9,7 @@ atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
backcall==0.1.0
|
||||
better-exceptions-fork==0.2.1.post6
|
||||
black==19.10b0
|
||||
bleach==3.1.4
|
||||
bleach==3.3.0
|
||||
bpylist2==3.0.2
|
||||
certifi==2020.4.5.1
|
||||
cffi==1.14.0
|
||||
@@ -42,6 +41,7 @@ mccabe==0.6.1
|
||||
modulegraph==0.18
|
||||
more-itertools==7.2.0
|
||||
multidict==4.7.6
|
||||
osxmetadata>=0.99.11
|
||||
packaging==19.0
|
||||
parso==0.6.2
|
||||
pathspec==0.7.0
|
||||
|
||||
11
setup.py
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# setup.py script for osxphotos
|
||||
#
|
||||
# Copyright (c) 2019, 2020 Rhet Turnbull, rturnbull+git@gmail.com
|
||||
# Copyright (c) 2019, 2020, 2021 Rhet Turnbull, rturnbull+git@gmail.com
|
||||
# All rights reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
@@ -45,15 +45,15 @@ with open(
|
||||
exec(f.read(), about)
|
||||
|
||||
# read README.md into long_description
|
||||
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
||||
with open(os.path.join(this_directory, "README.rst"), encoding="utf-8") as f:
|
||||
about["long_description"] = f.read()
|
||||
|
||||
setup(
|
||||
name="osxphotos",
|
||||
version=about["__version__"],
|
||||
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
|
||||
description="Export photos from Apple's macOS Photos app and query the Photos library database to access metadata about images.",
|
||||
long_description=about["long_description"],
|
||||
long_description_content_type="text/markdown",
|
||||
long_description_content_type="text/x-rst",
|
||||
author="Rhet Turnbull",
|
||||
author_email="rturnbull+git@gmail.com",
|
||||
url="https://github.com/RhetTbull/",
|
||||
@@ -68,6 +68,8 @@ setup(
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
install_requires=[
|
||||
@@ -81,6 +83,7 @@ setup(
|
||||
"wurlitzer>=2.0.1",
|
||||
"photoscript>=0.1.0",
|
||||
"toml>=0.10.0",
|
||||
"osxmetadata>=0.99.13",
|
||||
],
|
||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||
include_package_data=True,
|
||||
|
||||
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 577 KiB |
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 532 KiB |
BIN
tests/Test-10.16.0.1.photoslibrary/database/Photos.sqlite
Normal file
BIN
tests/Test-10.16.0.1.photoslibrary/database/Photos.sqlite-shm
Normal file
BIN
tests/Test-10.16.0.1.photoslibrary/database/Photos.sqlite-wal
Normal file
@@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>440</integer>
|
||||
<integer>400</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
BIN
tests/Test-10.16.0.1.photoslibrary/database/search/psi.sqlite
Normal file
@@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>BlacklistedMeaningsByMeaning</key>
|
||||
<dict/>
|
||||
<key>MePersonUUID</key>
|
||||
<string>39488755-78C0-40B2-B378-EDA280E1823C</string>
|
||||
<key>SceneWhitelist</key>
|
||||
<array>
|
||||
<string>Graduation</string>
|
||||
|
After Width: | Height: | Size: 574 KiB |