Compare commits
443 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42426b95ee | ||
|
|
262a6f31e7 | ||
|
|
04930c3644 | ||
|
|
44594a8e43 | ||
|
|
690d981f31 | ||
|
|
06ea8d1e6c | ||
|
|
c4e3c5a8be | ||
|
|
03f4e7cc34 | ||
|
|
0e54a08ae0 | ||
|
|
b71c752e9d | ||
|
|
521848f955 | ||
|
|
debb17c952 | ||
|
|
7819740f70 | ||
|
|
b9ffb0d8de | ||
|
|
d59852f594 | ||
|
|
085f482820 | ||
|
|
1cb8da96ce | ||
|
|
50016a9eca | ||
|
|
924f7325b4 | ||
|
|
181f678d9e | ||
|
|
6ce1b83ca2 | ||
|
|
a08a653f20 | ||
|
|
e1f1772080 | ||
|
|
d2a1f792e9 | ||
|
|
e7bd80e05f | ||
|
|
9089c0323c | ||
|
|
197e5663df | ||
|
|
f6dedaa619 | ||
|
|
0906dbe637 | ||
|
|
0629b3f6d6 | ||
|
|
55dfc0ec7d | ||
|
|
b7f8b26f1d | ||
|
|
9ca7dd50bc | ||
|
|
0e73d57bdf | ||
|
|
9de2c17e47 | ||
|
|
1bae6d33f1 | ||
|
|
a52b4d2f43 | ||
|
|
3e038bf124 | ||
|
|
870ed9c435 | ||
|
|
68e7ca3277 | ||
|
|
c9142c2156 | ||
|
|
7d923590ae | ||
|
|
5383ced1ca | ||
|
|
0e6c92dbd9 | ||
|
|
b00978c61a | ||
|
|
fb583e28e0 | ||
|
|
760386e3d7 | ||
|
|
51ba54971a | ||
|
|
2ffcf1e82b | ||
|
|
818f4f45a4 | ||
|
|
2cf19f6af1 | ||
|
|
ef82c6e32b | ||
|
|
0e9b9d6251 | ||
|
|
419b34ea73 | ||
|
|
f64c4ed374 | ||
|
|
1677f404d2 | ||
|
|
a612a363ed | ||
|
|
202bc1144b | ||
|
|
a0c654e43f | ||
|
|
2bb677dc19 | ||
|
|
e33805fe42 | ||
|
|
04ac0a1121 | ||
|
|
d2b0bd4e28 | ||
|
|
d754899563 | ||
|
|
4a81e643a7 | ||
|
|
b23e74f8f5 | ||
|
|
5dc766249a | ||
|
|
a895833c7f | ||
|
|
3f81a3c179 | ||
|
|
f1235f745f | ||
|
|
1ddb1de998 | ||
|
|
c472698b1d | ||
|
|
4e021a0731 | ||
|
|
bfbc156821 | ||
|
|
bfd6274602 | ||
|
|
3abaa5ae84 | ||
|
|
65115a50a9 | ||
|
|
06138e15d0 | ||
|
|
14710e3178 | ||
|
|
f705f09749 | ||
|
|
82c445f41e | ||
|
|
1b40e9d65f | ||
|
|
725f7c8735 | ||
|
|
7cc8578148 | ||
|
|
6adafb8ce7 | ||
|
|
ac47df8475 | ||
|
|
f680cf78ab | ||
|
|
c86e84c534 | ||
|
|
3fb611825c | ||
|
|
1cfdad0176 | ||
|
|
59ba325273 | ||
|
|
c4b7c2623f | ||
|
|
e5b2d2ee45 | ||
|
|
64c226b855 | ||
|
|
e3e1da2fd8 | ||
|
|
57b2f8a413 | ||
|
|
5a76a511db | ||
|
|
283f049780 | ||
|
|
c4743cc867 | ||
|
|
c429a860b1 | ||
|
|
1f748c829b | ||
|
|
dd08c7f701 | ||
|
|
77103193c0 | ||
|
|
16335a6bd6 | ||
|
|
e0f6d8ecf2 | ||
|
|
59c31ff88d | ||
|
|
93bf0c210c | ||
|
|
4f7642b1d2 | ||
|
|
773dca8494 | ||
|
|
3cd26e2e38 | ||
|
|
271761cf04 | ||
|
|
6eea552fb9 | ||
|
|
81dd1a7530 | ||
|
|
2eb6e70e57 | ||
|
|
6bcc67634c | ||
|
|
062d8eb206 | ||
|
|
f0d7496bc6 | ||
|
|
8e2b768236 | ||
|
|
48bf326994 | ||
|
|
159d1102aa | ||
|
|
dbb4dbc0a7 | ||
|
|
777e768243 | ||
|
|
70999a70b8 | ||
|
|
3a6b2c2c35 | ||
|
|
dfb80ba8d6 | ||
|
|
94b818b156 | ||
|
|
f1cea1498b | ||
|
|
345678577a | ||
|
|
fb4138cfe6 | ||
|
|
db5b34d589 | ||
|
|
8963af9229 | ||
|
|
2041789ff4 | ||
|
|
aec86f93ea | ||
|
|
57bfb03e05 | ||
|
|
c2b2476e38 | ||
|
|
fa2027d453 | ||
|
|
9d980e4917 | ||
|
|
673243c6cd | ||
|
|
7376223eb8 | ||
|
|
ecd0b8e22f | ||
|
|
c4a608b5bd | ||
|
|
4d9e21ea16 | ||
|
|
1ee3e035c4 | ||
|
|
b1c0fb3e82 | ||
|
|
de715d2afd | ||
|
|
607cf80dda | ||
|
|
0c8fbd69af | ||
|
|
c2335236be | ||
|
|
123340eada | ||
|
|
852a06f99b | ||
|
|
9f8da5c623 | ||
|
|
077d577c98 | ||
|
|
12f39dbaf5 | ||
|
|
6e9f709279 | ||
|
|
666b6cac33 | ||
|
|
e95c096784 | ||
|
|
745161fbd1 | ||
|
|
8216c33b59 | ||
|
|
a05e7be14e | ||
|
|
e27c40c772 | ||
|
|
e752f3c7a7 | ||
|
|
6f4cab6721 | ||
|
|
2d899ef045 | ||
|
|
4f17c8fb23 | ||
|
|
173a0fce28 | ||
|
|
b04ea8174d | ||
|
|
e40ecc45ad | ||
|
|
277b1614b9 | ||
|
|
88099de688 | ||
|
|
7d81b94c16 | ||
|
|
d627cfc4fa | ||
|
|
bf208bbe4b | ||
|
|
79ba6f813f | ||
|
|
141c0244e4 | ||
|
|
7e0276beb7 | ||
|
|
1bf11b0414 | ||
|
|
c23f3fc5e4 | ||
|
|
016297d2ff | ||
|
|
aa64283b55 | ||
|
|
3973c27238 | ||
|
|
2e32d62237 | ||
|
|
d497b94ad5 | ||
|
|
8c09ae82a4 | ||
|
|
632169f277 | ||
|
|
675371f0d7 | ||
|
|
7e2d09bf12 | ||
|
|
28c681aa96 | ||
|
|
5d39aa92df | ||
|
|
b4dbad5e74 | ||
|
|
b1b099257f | ||
|
|
63e8410841 | ||
|
|
2e1c91cd67 | ||
|
|
391b0a577b | ||
|
|
1d26ac9630 | ||
|
|
03b4f59549 | ||
|
|
9aa3ac3640 | ||
|
|
6339e3c70e | ||
|
|
4cc3220287 | ||
|
|
f32c4f4acd | ||
|
|
aba2ce0923 | ||
|
|
c209ceae2e | ||
|
|
94ac2bd04e | ||
|
|
d1b1d20bcf | ||
|
|
fb723fb8b7 | ||
|
|
fc7c61b11b | ||
|
|
a73db3a1bb | ||
|
|
d2dcbaaec4 | ||
|
|
08147e91d9 | ||
|
|
d034605784 | ||
|
|
64fd852535 | ||
|
|
3fbfc55e84 | ||
|
|
49317582c4 | ||
|
|
5ea01df69b | ||
|
|
4a9f8a9ef5 | ||
|
|
49adff1f3b | ||
|
|
377e165be4 | ||
|
|
07da8031c6 | ||
|
|
be363b9727 | ||
|
|
870a59a2fa | ||
|
|
500cf71f7e | ||
|
|
821e338b75 | ||
|
|
987c91a9ff | ||
|
|
233942c9b6 | ||
|
|
a0ab64a841 | ||
|
|
0cd8f32893 | ||
|
|
904acbc576 | ||
|
|
37dc023fcb | ||
|
|
876ff17e3f | ||
|
|
130df1a767 | ||
|
|
5d7dea3fc3 | ||
|
|
ca8397bc97 | ||
|
|
91023ac8ec | ||
|
|
0ad59e9e29 | ||
|
|
42c551de8a | ||
|
|
62d49a7138 | ||
|
|
bc5cd93e97 | ||
|
|
7bd1ba8075 | ||
|
|
64bb07a026 | ||
|
|
f1902b7fd4 | ||
|
|
8e3f8fc7d0 | ||
|
|
c588dcf0ba | ||
|
|
fa29f51aeb | ||
|
|
ee0b369086 | ||
|
|
2fc45c2468 | ||
|
|
15d2f45f0c | ||
|
|
df7b73212f | ||
|
|
5143b165b5 | ||
|
|
10097323e5 | ||
|
|
c0bd0ffc9f | ||
|
|
2cdec3fc78 | ||
|
|
1a46cdf63c | ||
|
|
83892e096a | ||
|
|
6a0b8b4a3f | ||
|
|
5957fde809 | ||
|
|
5711545b81 | ||
|
|
0758f84dc4 | ||
|
|
4b6c35b5f9 | ||
|
|
d7a9ad1d0a | ||
|
|
bb96c35672 | ||
|
|
0880e5b9e8 | ||
|
|
87af23d98c | ||
|
|
61943d051b | ||
|
|
ef1daf5922 | ||
|
|
bb98cff608 | ||
|
|
620ba9ce03 | ||
|
|
86d94ad310 | ||
|
|
b8cf21ae82 | ||
|
|
7accfdb066 | ||
|
|
99f4394f8e | ||
|
|
748aed96cb | ||
|
|
9161739ee6 | ||
|
|
71cf8be94a | ||
|
|
b48133cd83 | ||
|
|
6b5a57fae9 | ||
|
|
24ccf798c2 | ||
|
|
a298772515 | ||
|
|
2d68594b78 | ||
|
|
b026147c9a | ||
|
|
186a5b77d0 | ||
|
|
518f855a9b | ||
|
|
0d2067787c | ||
|
|
0448a42329 | ||
|
|
a724e15dd6 | ||
|
|
be8fe9d059 | ||
|
|
bd6656107b | ||
|
|
a54e051d41 | ||
|
|
7cde52bf9b | ||
|
|
96037508c1 | ||
|
|
9f2268fb2b | ||
|
|
df167c00eb | ||
|
|
e8f9cda0c6 | ||
|
|
d4a951f547 | ||
|
|
f24e4a7e3c | ||
|
|
98b84c17f1 | ||
|
|
78c411a643 | ||
|
|
6bdf15b41e | ||
|
|
a0fcec2a7a | ||
|
|
63834ab8ab | ||
|
|
b23cfa32bb | ||
|
|
0e22ce54ab | ||
|
|
0f41588701 | ||
|
|
442b542794 | ||
|
|
88fae81b19 | ||
|
|
c4fec00f67 | ||
|
|
9a0cc3e8fa | ||
|
|
3ed2362fe3 | ||
|
|
cd8dd552a4 | ||
|
|
64379f313e | ||
|
|
dc53480126 | ||
|
|
3e06e0e344 | ||
|
|
aa9f6520d4 | ||
|
|
3a56e05c85 | ||
|
|
454a813908 | ||
|
|
31e162ba94 | ||
|
|
5b8d51da38 | ||
|
|
846ea89012 | ||
|
|
dc0bbd5fd6 | ||
|
|
91804d53ea | ||
|
|
3d26206d91 | ||
|
|
92d9dfaef2 | ||
|
|
51025e7f8b | ||
|
|
d52e5e9316 | ||
|
|
3711b3f7f1 | ||
|
|
48c229b52c | ||
|
|
aad435da36 | ||
|
|
9c60259089 | ||
|
|
131105d82c | ||
|
|
f54205ff49 | ||
|
|
1d14fc8041 | ||
|
|
4aec01ad1d | ||
|
|
3630643a0e | ||
|
|
44966c6736 | ||
|
|
7c4b28a35c | ||
|
|
1cdf4addad | ||
|
|
50fa851f23 | ||
|
|
a483b8a900 | ||
|
|
dd6d519135 | ||
|
|
9371db094e | ||
|
|
d9a82f29c7 | ||
|
|
3f57514fa3 | ||
|
|
a5a155bd05 | ||
|
|
c8ea0b0452 | ||
|
|
81fd51c793 | ||
|
|
648d399524 | ||
|
|
345c052353 | ||
|
|
952f1a6c3c | ||
|
|
7ae5b8aae7 | ||
|
|
2e189d771e | ||
|
|
7fa7de1563 | ||
|
|
70d68a25ba | ||
|
|
bfc4371d9e | ||
|
|
6a288676a1 | ||
|
|
874ad2fa34 | ||
|
|
a233167471 | ||
|
|
21dc0d388f | ||
|
|
eff8e7a63f | ||
|
|
03f8b2bc6e | ||
|
|
e215c200c7 | ||
|
|
ae5b02f563 | ||
|
|
aa1a96d201 | ||
|
|
d9f24307ac | ||
|
|
958f8c343a | ||
|
|
70cf4c9f92 | ||
|
|
2d3344ee34 | ||
|
|
b4bc906b6a | ||
|
|
520a15fac6 | ||
|
|
032dff8967 | ||
|
|
3c36b0fb33 | ||
|
|
d51d7a41e4 | ||
|
|
60c926fea5 | ||
|
|
db27aac14b | ||
|
|
d17454772c | ||
|
|
9c9e73ba96 | ||
|
|
e21a78c2b3 | ||
|
|
de0fbf2bb9 | ||
|
|
b330e27fb8 | ||
|
|
a941f66d62 | ||
|
|
d77eba12b2 | ||
|
|
de94fd76de | ||
|
|
1026473684 | ||
|
|
3f9c9893c3 | ||
|
|
574cdd65a3 | ||
|
|
5b9547669e | ||
|
|
35b5bbd13d | ||
|
|
6870ad0d8e | ||
|
|
17ac5949e1 | ||
|
|
ffb9af1965 | ||
|
|
595307a003 | ||
|
|
79a50b9e50 | ||
|
|
515df0a5dc | ||
|
|
63bfa92563 | ||
|
|
44a1e3e7a7 | ||
|
|
6c84e476cc | ||
|
|
14fbe5e068 | ||
|
|
ebac9d0bfb | ||
|
|
29716c5272 | ||
|
|
fbe8229103 | ||
|
|
5ee6affc05 | ||
|
|
b3a7869bd3 | ||
|
|
e5f1c29974 | ||
|
|
70848e1ff6 | ||
|
|
4b7a53faa8 | ||
|
|
a78dd80af4 | ||
|
|
1316866dc4 | ||
|
|
30273509d4 | ||
|
|
15a3e69015 | ||
|
|
2691902d5c | ||
|
|
da47821fae | ||
|
|
6f38e2da49 | ||
|
|
857e3db6cc | ||
|
|
7ed3115f36 | ||
|
|
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 |
@@ -166,6 +166,144 @@
|
||||
"research",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davidjroos",
|
||||
"name": "davidjroos ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15630844?v=4",
|
||||
"profile": "https://github.com/davidjroos",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neilpa",
|
||||
"name": "Neil Pankey",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/42419?v=4",
|
||||
"profile": "https://neilpa.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AaronVanGeffen",
|
||||
"name": "Aaron van Geffen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/604665?v=4",
|
||||
"profile": "https://aaronweb.net/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ubrandes",
|
||||
"name": "ubrandes ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59647284?v=4",
|
||||
"profile": "https://github.com/ubrandes",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pdewost",
|
||||
"name": "Philippe Dewost",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17090228?v=4",
|
||||
"profile": "http://blog.dewost.com/",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"example",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kaduskj",
|
||||
"name": "kaduskj",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/983067?v=4",
|
||||
"profile": "https://github.com/kaduskj",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mkirkland4874",
|
||||
"name": "mkirkland4874",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36466711?v=4",
|
||||
"profile": "https://github.com/mkirkland4874",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"example"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jcommisso07",
|
||||
"name": "Joseph Commisso",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3111054?v=4",
|
||||
"profile": "https://github.com/jcommisso07",
|
||||
"contributions": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dssinger",
|
||||
"name": "David Singer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1817903?v=4",
|
||||
"profile": "https://github.com/dssinger",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "oPromessa",
|
||||
"name": "oPromessa",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/21261491?v=4",
|
||||
"profile": "https://github.com/oPromessa",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "spencerc99",
|
||||
"name": "Spencer Chang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14796580?v=4",
|
||||
"profile": "http://spencerchang.me",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dgleich",
|
||||
"name": "David Gleich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
|
||||
"profile": "https://www.cs.purdue.edu/homes/dgleich",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alandefreitas",
|
||||
"name": "Alan de Freitas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
|
||||
"profile": "https://alandefreitas.github.io/alandefreitas/",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hyfen",
|
||||
"name": "Andrew Louis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
|
||||
"profile": "https://hyfen.net",
|
||||
"contributions": [
|
||||
"doc", "code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neebah",
|
||||
"name": "neebah",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
|
||||
"profile": "https://github.com/neebah",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
** Before submitting a bug report, please ensure you are running the most recent version of osxphotos and that the bug is reproducible on the latest version **
|
||||
|
||||
- If you installed with pipx: `pipx upgrade osxphotos`
|
||||
- If you installed with pip: `pip install --upgrade osxphotos`
|
||||
- If you installed the pre-built binary, download and install the latest [release](https://github.com/RhetTbull/osxphotos/releases)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. What' the full command line you used with osxphotos?
|
||||
2. What was the error output?
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. which version of macOS]
|
||||
- osxphotos version (`osxphotos --version`)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -4,13 +4,13 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macOS-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.7, 3.8]
|
||||
os: [macos-10.15]
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r dev_requirements.txt
|
||||
pip install -r requirements.txt
|
||||
# - name: Lint with flake8
|
||||
# run: |
|
||||
@@ -31,6 +32,4 @@ jobs:
|
||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install pytest
|
||||
pip install pytest-mock
|
||||
python -m pytest tests/
|
||||
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
.metrics
|
||||
.DS_store
|
||||
__pycache__
|
||||
.coverage
|
||||
.condaauto
|
||||
t.out
|
||||
.vscode/
|
||||
.tox/
|
||||
.idea/
|
||||
dist/
|
||||
build/
|
||||
working/
|
||||
osxphotos.egg-info/
|
||||
.mypy_cache/
|
||||
cli.spec
|
||||
*.pyc
|
||||
docsrc/_build/
|
||||
venv/
|
||||
.python-version
|
||||
3
.isort.cfg
Normal file
3
.isort.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
[settings]
|
||||
profile=black
|
||||
multi_line_output=3
|
||||
1058
CHANGELOG.md
1058
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Contributing
|
||||
|
||||
Contributions of all kinds are welcome! You don't need to know python to contribute to this project. For example, documentation updates are just as welcome as code!
|
||||
|
||||
Please explore open [issues](https://github.com/RhetTbull/osxphotos/issues), [discussions](https://github.com/RhetTbull/osxphotos/discussions), and the project [wiki](https://github.com/RhetTbull/osxphotos/wiki) to learn more about the project.
|
||||
|
||||
If you want to contribute source code, I recommend you explore the [wiki](https://github.com/RhetTbull/osxphotos/wiki/Structure-of-the-code) to learn about the source structure first.
|
||||
|
||||
See the [README.md](tests/README.md) in the tests directory before running any tests.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Be nice to each other. Treat everyone with dignity and respect.
|
||||
|
||||
Abusive behavior of any kind will not be tolerated here.
|
||||
@@ -1,2 +1,6 @@
|
||||
include README.md
|
||||
include osxphotos/templates/*
|
||||
include README.rst
|
||||
include osxphotos/templates/*
|
||||
include osxphotos/phototemplate.tx
|
||||
include osxphotos/phototemplate.md
|
||||
include osxphotos/queries/*
|
||||
12
README.rst
12
README.rst
@@ -16,8 +16,9 @@ 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).
|
||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).
|
||||
|
||||
If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.
|
||||
|
||||
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.
|
||||
@@ -109,6 +110,8 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options; if...
|
||||
repl Run interactive osxphotos shell
|
||||
tutorial Display osxphotos tutorial.
|
||||
|
||||
To get help on a specific command, use ``osxphotos help <command_name>``
|
||||
|
||||
@@ -146,6 +149,11 @@ export default library using 'country name/year' as output directory (but use "N
|
||||
|
||||
``osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose``
|
||||
|
||||
find all videos larger than 200MB and add them to Photos album "Big Videos" creating the album if necessary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``osxphotos query --only-movies --min-size 200MB --add-to-album "Big Videos"``
|
||||
|
||||
Example uses of the package
|
||||
---------------------------
|
||||
|
||||
|
||||
12
build.sh
Executable file
12
build.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# script to help build osxphotos release
|
||||
# this is unique to my own dev setup
|
||||
|
||||
# source venv/bin/activate
|
||||
rm -rf dist; rm -rf build
|
||||
python3 utils/update_readme.py
|
||||
(cd docsrc && make github && make pdf)
|
||||
# python3 setup.py sdist bdist_wheel
|
||||
python3 -m build
|
||||
./make_cli_exe.sh
|
||||
2
cli.py
2
cli.py
@@ -12,7 +12,7 @@
|
||||
|
||||
"""
|
||||
|
||||
from osxphotos.__main__ import cli
|
||||
from osxphotos.cli import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
10
dev_requirements.txt
Normal file
10
dev_requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
build
|
||||
m2r2
|
||||
pyinstaller==4.4
|
||||
pytest-mock
|
||||
pytest==6.2.4
|
||||
sphinx_click
|
||||
sphinx_rtd_theme
|
||||
twine
|
||||
wheel
|
||||
Sphinx
|
||||
4
docs/.buildinfo
Normal file
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: 58505ca56d322ccc59c38bc440ccc347
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
105
docs/_modules/index.html
Normal file
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.43.9 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
||||
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.2</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
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.41.4 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.5.2</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2246
docs/_modules/osxphotos/photoinfo/_photoinfo_export.html
Normal file
2246
docs/_modules/osxphotos/photoinfo/_photoinfo_export.html
Normal file
File diff suppressed because it is too large
Load Diff
220
docs/_modules/osxphotos/photoinfo/_photoinfo_scoreinfo.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.41.4 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.5.2</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
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.41.4 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.5.2</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1511
docs/_modules/osxphotos/photoinfo/photoinfo.html
Normal file
1511
docs/_modules/osxphotos/photoinfo/photoinfo.html
Normal file
File diff suppressed because it is too large
Load Diff
3671
docs/_modules/osxphotos/photosdb/photosdb.html
Normal file
3671
docs/_modules/osxphotos/photosdb/photosdb.html
Normal file
File diff suppressed because it is too large
Load Diff
6
docs/_sources/cli.rst.txt
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
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
5
docs/_sources/modules.rst.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
osxphotos
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
13
docs/_sources/reference.rst.txt
Normal file
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:
|
||||
354
docs/_sources/tutorial.md.txt
Normal file
354
docs/_sources/tutorial.md.txt
Normal file
@@ -0,0 +1,354 @@
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
|
||||
# OSXPhotos Tutorial
|
||||
|
||||
## Tutorial
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:END -->
|
||||
|
||||
The design philosophy for osxphotos is "make the easy things easy and make the hard things possible". To "make the hard things possible", osxphotos is very flexible and has many, many configuration options -- the `export` command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the `export` command which exports photos from the library so that's the focus of this tutorial.
|
||||
|
||||
### Export your photos
|
||||
|
||||
`osxphotos export /path/to/export`
|
||||
|
||||
This command exports all your photos to the `/path/to/export` directory.
|
||||
|
||||
**Note**: osxphotos uses the term 'photo' to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple "Live Photo" which is an image and an associated "live preview" video file), a JPEG image with an associated RAW image, etc.
|
||||
|
||||
### Export by date
|
||||
|
||||
While the previous command will export all your photos (and videos--see note above), it probably doesn't do exactly what you want. In the previous example, all the photos will be exported to a single folder: `/path/to/export`. If you have a large library with thousands of images and videos, this likely isn't very useful. You can use the `--export-by-date` option to export photos to a folder structure organized by year, month, day, e.g. `2021/04/21`:
|
||||
|
||||
`osxphotos export /path/to/export --export-by-date`
|
||||
|
||||
With this command, a photo that was created on 31 May 2015 would be exported to: `/path/to/export/2015/05/31`
|
||||
|
||||
### Specify directory structure
|
||||
|
||||
If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START -->template system<!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --> that allows you to specify the directory structure using the `--directory` option. For example, this command exported to a directory structure that looks like: `2015/May` (4-digit year / month name):
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{created.month}"`
|
||||
|
||||
The string following `--directory` is an `osxphotos template string`. Template strings are widely used throughout osxphotos and it's worth your time to learn more about them. In a template string, the values between the curly braces, e.g. `{created.year}` are replaced with metadata from the photo being exported. In this case, `{created.year}` is the 4-digit year of the photo's creation date and `{created.month}` is the full month name in the user's locale (e.g. `May`, `mai`, etc.). In the osxphotos template system these are referred to as template fields. The text not included between `{}` pairs is interpreted literally, in this case `/`, is a directory separator.
|
||||
|
||||
osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the `--directory` template, you could thus export photos organized by country name:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country}"`
|
||||
|
||||
Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).
|
||||
|
||||
`osxphotos export /path/to/export --directory "{created.year}/{place.name.country,No-Country}"`
|
||||
|
||||
The value after the ',' in the template string is the default value, in this case 'No-Country'. **Note**: If you don't specify a default value and a template field is null, osxphotos will use "_" (underscore character) as the default.
|
||||
|
||||
Some template fields, such as `{keyword}`, may expand to more than one value. For example, if a photo has keywords of "Travel" and "Vacation", `{keyword}` would expand to "Travel", "Vacation". When used with `--directory`, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --directory "{keyword}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel/IMG_1234.JPG
|
||||
/path/to/export/Vacation/IMG_1234.JPG
|
||||
|
||||
### Specify exported filename
|
||||
|
||||
By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like `IMG_1234.JPG` or `DSC05678.dng`. osxphotos allows you to specify a custom filename template using the `--filename` option in the same way as `--directory` allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{title}"`
|
||||
|
||||
The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{title,{original_name}}"
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
Use photo's title as the filename <──────┘ ││ │
|
||||
││ │
|
||||
Value after comma will be used <───────┘│ │
|
||||
if title is blank │ │
|
||||
│ │
|
||||
The default value can be <────┘ │
|
||||
another template field │
|
||||
│
|
||||
Use photo's original name if no title <──────┘
|
||||
```
|
||||
|
||||
The osxphotos template system also allows for limited conditional logic of the type "If a condition is true then do one thing, otherwise, do a different thing". For example, you can use the `--filename` option to name files that are marked as "Favorites" in Photos differently than other files. For example, to add a "#" to the name of every photo that's a favorite:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
|
||||
│ │ │││
|
||||
│ │ │││
|
||||
Use photo's original name as filename <──┘ │ │││
|
||||
│ │││
|
||||
'favorite' is True if photo is a Favorite, <───────┘ │││
|
||||
otherwise, False │││
|
||||
│││
|
||||
'?' specifies a conditional <─────────────┘││
|
||||
││
|
||||
Value immediately following ? will be used if <──────┘│
|
||||
preceding template field is True or non-blank │
|
||||
│
|
||||
Value immediately following comma will be used if <──────┘
|
||||
template field is False or blank (null); in this case
|
||||
no value is specified so a blank string "" will be used
|
||||
```
|
||||
|
||||
Like with `--directory`, using a multi-valued template field such as `{keyword}` may result in more than one copy of a photo being exported. For example, if `IMG_1234.JPG` has keywords `Travel`, and `Vacation` and you run the following command:
|
||||
|
||||
`osxphotos export /path/to/export --filename "{keyword}-{original_name}"`
|
||||
|
||||
the exported files would be:
|
||||
|
||||
/path/to/export/Travel-IMG_1234.JPG
|
||||
/path/to/export/Vacation-IMG_1234.JPG
|
||||
|
||||
### Edited photos
|
||||
|
||||
If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append "_edited" to the edited image. For example, if the original image was named `IMG_1234.JPG`, osxphotos will export the original as `IMG_1234.JPG` and the edited version as `IMG_1234_edited.jpeg`. **Note:** Photos changes the extension of edited images to ".jpeg" even if the original was named ".JPG". You can change the suffix appended to edited images using the `--edited-suffix` option:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_EDIT"`
|
||||
|
||||
In this example, the edited image would be named `IMG_1234_EDIT.jpeg`. Like many options in osxphotos, the `--edited-suffix` option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:
|
||||
|
||||
`osxphotos export /path/to/export --edited-suffix "_{modified.year}-{modified.mm}-{modified.dd}"`
|
||||
|
||||
In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: `IMG_1234_2021-04-21.jpeg`.
|
||||
|
||||
You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using `--skip-edited`:
|
||||
|
||||
`osxphotos export /path/to/export --skip-edited`
|
||||
|
||||
You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the `--skip-original-if-edited` option:
|
||||
|
||||
`osxphotos export /path/to/export --skip-original-if-edited`
|
||||
|
||||
As mentioned above, Photos renames JPEG images that have been edited with the ".jpeg" extension. Some applications use ".JPG" and others use ".jpg" or ".JPEG". You can use the `--jpeg-ext` option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. `--jpeg-ext jpg` to use '.jpg' for all JPEGs.
|
||||
|
||||
`osxphotos export /path/to/export --jpeg-ext jpg`
|
||||
|
||||
### Specifying the Photos library
|
||||
|
||||
All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the "Option" key while opening Photos, you can select an alternate Photos library. If you don't specify which library to use, osxphotos will try find the last opened library. Occasionally it can't determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the `--db` option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).
|
||||
|
||||
`osxphotos export /path/to/export --db ~/Pictures/MyAlternateLibrary.photoslibrary`
|
||||
|
||||
### Missing photos
|
||||
|
||||
osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.
|
||||
|
||||
If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the `--download-missing` option. `--download-missing` uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos' AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There's also an experimental `--use-photokit` option that will communicate with Photos using a different "PhotoKit" interface. This option must be used together with `--download-missing`:
|
||||
|
||||
`osxphotos export /path/to/export --download-missing`
|
||||
|
||||
`osxphotos export /path/to/export --download-missing --use-photokit`
|
||||
|
||||
### Exporting to external disks
|
||||
|
||||
If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the `--retry` option so that osxphotos will automatically retry the export. Use `--retry` with a number that specifies the number of times to retry the export:
|
||||
|
||||
`osxphotos export /path/to/export --retry 3`
|
||||
|
||||
In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.
|
||||
|
||||
### Exporting metadata with exported photos
|
||||
|
||||
Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos' native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free [`exiftool`](https://exiftool.org/) app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the `--exiftool` option to write metadata to exported photos:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool`
|
||||
|
||||
This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with `--exiftool` to modify the metadata that is written by `exiftool`. For example, you can use the `--keyword-template` option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:
|
||||
|
||||
```txt
|
||||
osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
|
||||
│ │
|
||||
│ │
|
||||
folder_album results in the folder(s) <──┘ │
|
||||
and album a photo is contained in │
|
||||
│
|
||||
The value in () is used as the path separator <───────┘
|
||||
for joining the folders and albums. For example,
|
||||
if photo is in Folder1/Folder2/Album, (>) produces
|
||||
"Folder1>Folder2>Album" which some programs, such as
|
||||
Lightroom Classic, treat as hierarchal keywords
|
||||
```
|
||||
|
||||
The above command will write all the regular metadata that `--exiftool` normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form "Folder1>Folder2>Album". If you did not include the `(>)` in the template string (e.g. `{folder_album}`), folder_album would render in form "Folder1/Folder2/Album".
|
||||
|
||||
A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the `{label}`. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image's metadata:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label}"`
|
||||
|
||||
**Note**: When evaluating templates for `--directory` and `--filename`, osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as `--keyword-template`, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
|
||||
|
||||
`osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"`
|
||||
|
||||
### Sidecar files
|
||||
|
||||
Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the `--sidecar` option. For example, to output metadata to XMP sidecars:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP`
|
||||
|
||||
Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`.
|
||||
|
||||
Sidecar files are named "photoname.ext.sidecar_ext". For example, if the photo is named `IMG_1234.JPG` and the sidecar format is XMP, the sidecar would be named `IMG_1234.JPG.XMP`. Some applications expect the sidecar in this case to be named `IMG_1234.XMP`. You can use the `-sidecar-drop-ext` option to force osxphotos to name the sidecar files in this manner:
|
||||
|
||||
`osxphotos export /path/to/export --sidecar XMP -sidecar-drop-ext`
|
||||
|
||||
### Updating a previous export
|
||||
|
||||
If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the `--update` option. When `osxphotos export` is run, it creates a database file named `.osxphotos_export.db` in the export folder. (**Note** Because the filename starts with a ".", you won't see it in Finder which treats "dot-files" like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the `--update` option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:
|
||||
|
||||
`osxphotos export /path/to/export --update`
|
||||
|
||||
will read the export database located in `/path/to/export/.osxphotos_export.db` and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the `--update` option even if it's never been run before. If the database isn't found, osxphotos will create it. If you run `osxphotos export` without `--update` in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use `--update`.
|
||||
|
||||
If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of `--update`, you can use the `--only-new` with `--update` to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without `--only-new`, osxphotos would see that previously exported files are missing and re-export them.
|
||||
|
||||
`osxphotos export /path/to/export --update --only-new`
|
||||
|
||||
If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with `--update`, you should use the `--ignore-signature` option. `--ignore-signature` instructs osxphotos to ignore the file's signature (for example, size and date modified) when deciding which files should be updated with `--update`. If you edit a file in the export directory and then run `--update` without `--ignore-signature`, osxphotos will see that the file is different than the one in the Photos library and re-export it.
|
||||
|
||||
`osxphotos export /path/to/export --update --ignore-signature`
|
||||
|
||||
### Dry Run
|
||||
|
||||
You can use the `--dry-run` option to have osxphotos "dry run" or test an export without actually exporting any files. When combined with the `--verbose` option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your `--directory` and `--filename` templates are correct, `--dry-run --verbose` will print out the name of each file being exported.
|
||||
|
||||
`osxphotos export /path/to/export --dry-run --verbose`
|
||||
|
||||
### Creating a report of all exported files
|
||||
|
||||
You can use the `--report` option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the `--report` option:
|
||||
|
||||
`osxphotos export /path/to/export --report export.csv`
|
||||
|
||||
### Exporting only certain photos
|
||||
|
||||
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
|
||||
|
||||
For example, to export only photos with keyword `Travel`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel"`
|
||||
|
||||
Like many options in osxphotos, `--keyword` (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword `Travel` *or* keyword `Vacation`:
|
||||
|
||||
`osxphotos export /path/to/export --keyword "Travel" --keyword "Vacation"`
|
||||
|
||||
To export only photos contained in the album "Summer Vacation":
|
||||
|
||||
`osxphotos export /path/to/export --album "Summer Vacation"`
|
||||
|
||||
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
|
||||
|
||||
`osxphotos export /path/to/export --portrait`
|
||||
|
||||
You can also export photos in a certain date range:
|
||||
|
||||
`osxphotos export /path/to/export --from-date "2020-01-01" --to-date "2020-02-28"`
|
||||
|
||||
### Converting images to JPEG on export
|
||||
|
||||
Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the `--convert-to-jpeg` option. You can specify the JPEG quality (0: worst, 1.0: best) using `--jpeg-quality`. For example:
|
||||
|
||||
`osxphotos export /path/to/export --convert-to-jpeg --jpeg-quality 0.9`
|
||||
|
||||
### Finder attributes
|
||||
|
||||
In addition to using `exiftool` to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.
|
||||
|
||||
osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the `tag:tagname` syntax:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords`
|
||||
|
||||
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords --keyword-template "{label}"`
|
||||
|
||||
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
|
||||
|
||||
For example, to set Finder comment to the photo's title and description:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{newline}{descr}"`
|
||||
|
||||
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
|
||||
|
||||
Explanation of the template string:
|
||||
|
||||
```txt
|
||||
{title}{title?{descr?{newline},},}{descr}
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──> insert title │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└───> is there a title?
|
||||
│ │ │ │ │
|
||||
└───> if so, is there a description?
|
||||
│ │ │ │
|
||||
└───> if so, insert new line
|
||||
│ │ │
|
||||
└───> if descr is blank, insert nothing
|
||||
│ │
|
||||
└───> if title is blank, insert nothing
|
||||
│
|
||||
└───> finally, insert description
|
||||
```
|
||||
|
||||
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
|
||||
|
||||
The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.
|
||||
|
||||
See Extended Attributes section in the help for `osxphotos export` for additional information about this feature.
|
||||
|
||||
### Saving and loading options
|
||||
|
||||
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
|
||||
|
||||
To save the configuration:
|
||||
|
||||
`osxphotos export /path/to/export <all your options here> --update --save-config osxphotos.toml`
|
||||
|
||||
Then the next to you run osxphotos, you can simply do this:
|
||||
|
||||
`osxphotos export /path/to/export --load-config osxphotos.toml`
|
||||
|
||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||
|
||||
### An example from an actual osxphotos user
|
||||
|
||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||
|
||||
I usually import my iPhone’s photo roll on a more or less regular basis, and it
|
||||
includes photos and videos. As a result, the size ot my Photos library may rise
|
||||
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
|
||||
quite good keyword management system.
|
||||
|
||||
After a while, I want to take most of the videos out of the library and move them
|
||||
to a separate "videos" folder on a different folder / volume. As I might want to
|
||||
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
|
||||
its internal library tagging system, I will use osxphotos to do just this.
|
||||
|
||||
Picking the videos can be left to Photos, using a smart folder for instance. Then
|
||||
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
|
||||
to spot all videos created on my iPhone using the Quik application (now part of
|
||||
GoPro).
|
||||
|
||||
I want to retrieve my keywords only and make sure they populate the Finder tags, as
|
||||
well as export all the persons identified in the videos by Photos. I also want to
|
||||
merge any keywords or persons already in the video metadata with the exported
|
||||
metadata.
|
||||
|
||||
Keeping Photo’s edited titles and descriptions and putting both in the Finder
|
||||
comments field in a readable manner is also enabled.
|
||||
|
||||
And I want to keep the file’s creation date (using `--touch-file`).
|
||||
|
||||
Finally, use `--strip` to remove any leading or trailing whitespace from processed
|
||||
template fields.
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
701
docs/_static/alabaster.css
vendored
Normal file
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;
|
||||
}
|
||||
}
|
||||
905
docs/_static/basic.css
vendored
Normal file
905
docs/_static/basic.css
vendored
Normal file
@@ -0,0 +1,905 @@
|
||||
/*
|
||||
* 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 p.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, .figure.align-left, object.align-left {
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
img.align-right, figure.align-right, .figure.align-right, object.align-right {
|
||||
clear: right;
|
||||
float: right;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
img.align-center, figure.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
img.align-default, figure.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,
|
||||
aside.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,
|
||||
aside.sidebar > :last-child,
|
||||
div.topic > :last-child,
|
||||
div.admonition > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sidebar::after,
|
||||
aside.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, figure {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.figure p.caption, figcaption {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-number,
|
||||
figcaption span.caption-number {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-text,
|
||||
figcaption 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;
|
||||
}
|
||||
|
||||
/* -- object description styles --------------------------------------------- */
|
||||
|
||||
.sig {
|
||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
}
|
||||
|
||||
.sig-name, code.descname {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
code.descname {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.sig-prename, code.descclassname {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.optional {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.sig-paren {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.sig-param.n {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* C++ specific styling */
|
||||
|
||||
.sig-inline.c-texpr,
|
||||
.sig-inline.cpp-texpr {
|
||||
font-family: unset;
|
||||
}
|
||||
|
||||
.sig.c .k, .sig.c .kt,
|
||||
.sig.cpp .k, .sig.cpp .kt {
|
||||
color: #0033B3;
|
||||
}
|
||||
|
||||
.sig.c .m,
|
||||
.sig.cpp .m {
|
||||
color: #1750EB;
|
||||
}
|
||||
|
||||
.sig.c .s, .sig.c .sc,
|
||||
.sig.cpp .s, .sig.cpp .sc {
|
||||
color: #067D17;
|
||||
}
|
||||
|
||||
|
||||
/* -- 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;
|
||||
}
|
||||
|
||||
.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 0.5em;
|
||||
content: ":";
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
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.highlight span.gp { /* gp: Generic.Prompt */
|
||||
user-select: none;
|
||||
-webkit-user-select: text; /* Safari fallback only */
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+ */
|
||||
}
|
||||
|
||||
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.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
1
docs/_static/custom.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/* This file intentionally left blank. */
|
||||
323
docs/_static/doctools.js
vendored
Normal file
323
docs/_static/doctools.js
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
|
||||
*/
|
||||
jQuery.urldecode = function(x) {
|
||||
if (!x) {
|
||||
return 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;
|
||||
}
|
||||
break;
|
||||
case 39: // right
|
||||
var nextHref = $('link[rel="next"]').prop('href');
|
||||
if (nextHref) {
|
||||
window.location.href = nextHref;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// quick alias for translations
|
||||
_ = Documentation.gettext;
|
||||
|
||||
$(document).ready(function() {
|
||||
Documentation.init();
|
||||
});
|
||||
12
docs/_static/documentation_options.js
vendored
Normal file
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.44.1',
|
||||
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
BIN
docs/_static/file.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 286 B |
10872
docs/_static/jquery-3.5.1.js
vendored
Normal file
10872
docs/_static/jquery-3.5.1.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
docs/_static/jquery.js
vendored
Normal file
2
docs/_static/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
297
docs/_static/language_data.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 is copied as a separate JS file, is available */
|
||||
|
||||
/**
|
||||
* 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
BIN
docs/_static/minus.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
BIN
docs/_static/plus.png
vendored
Normal file
BIN
docs/_static/plus.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
82
docs/_static/pygments.css
vendored
Normal file
82
docs/_static/pygments.css
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .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 */
|
||||
529
docs/_static/searchtools.js
vendored
Normal file
529
docs/_static/searchtools.js
vendored
Normal file
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* 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></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);
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
}, 5);
|
||||
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
|
||||
$.ajax({url: requestUrl,
|
||||
dataType: "text",
|
||||
complete: function(jqxhr, textstatus) {
|
||||
var data = jqxhr.responseText;
|
||||
if (data !== '' && data !== undefined) {
|
||||
var summary = Search.makeSearchSummary(data, searchterms, hlterms);
|
||||
if (summary) {
|
||||
listItem.append(summary);
|
||||
}
|
||||
}
|
||||
Search.output.append(listItem);
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
}, 5);
|
||||
}});
|
||||
} else {
|
||||
// no source available, just display title
|
||||
Search.output.append(listItem);
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
// 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 iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
|
||||
var match = objects[prefix][iMatch];
|
||||
var name = match[4];
|
||||
var fullname = (prefix ? prefix + '.' : '') + name;
|
||||
var fullnameLower = fullname.toLowerCase()
|
||||
if (fullnameLower.indexOf(object) > -1) {
|
||||
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 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||
*/
|
||||
escapeRegExp : function(string) {
|
||||
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
var word_regex = this.escapeRegExp(word);
|
||||
for (var w in terms) {
|
||||
if (w.match(word_regex) && !terms[word]) {
|
||||
_o.push({files: terms[w], score: Scorer.partialTerm})
|
||||
}
|
||||
}
|
||||
for (var w in titleterms) {
|
||||
if (w.match(word_regex) && !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);
|
||||
if (text == "") {
|
||||
return null;
|
||||
}
|
||||
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 = $('<p class="context"></p>').text(excerpt);
|
||||
$.each(hlwords, function() {
|
||||
rv = rv.highlightText(this, 'highlighted');
|
||||
});
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
Search.init();
|
||||
});
|
||||
2027
docs/_static/underscore-1.12.0.js
vendored
Normal file
2027
docs/_static/underscore-1.12.0.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2042
docs/_static/underscore-1.13.1.js
vendored
Normal file
2042
docs/_static/underscore-1.13.1.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
999
docs/_static/underscore-1.3.1.js
vendored
Normal file
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);
|
||||
6
docs/_static/underscore.js
vendored
Normal file
6
docs/_static/underscore.js
vendored
Normal file
File diff suppressed because one or more lines are too long
109
docs/cli.html
Normal file
109
docs/cli.html
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||
|
||||
<title>osxphotos command line interface (CLI) — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="osxphotos package" href="reference.html" />
|
||||
<link rel="prev" title="Welcome to osxphotos’s documentation!" href="index.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">
|
||||
|
||||
<section id="osxphotos-command-line-interface-cli">
|
||||
<h1>osxphotos command line interface (CLI)<a class="headerlink" href="#osxphotos-command-line-interface-cli" title="Permalink to this headline">¶</a></h1>
|
||||
</section>
|
||||
|
||||
|
||||
</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 class="current">
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">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>Previous: <a href="index.html" title="previous chapter">Welcome to osxphotos’s documentation!</a></li>
|
||||
<li>Next: <a href="reference.html" title="next chapter">osxphotos package</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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/cli.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
104
docs/genindex.html
Normal file
104
docs/genindex.html
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Index — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="#" />
|
||||
<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 id="index">Index</h1>
|
||||
|
||||
<div class="genindex-jumpbox">
|
||||
|
||||
</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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
370
docs/index.html
Normal file
370
docs/index.html
Normal file
@@ -0,0 +1,370 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="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">
|
||||
|
||||
<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>
|
||||
</section>
|
||||
<section id="osxphotos">
|
||||
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<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>
|
||||
</section>
|
||||
<section id="supported-operating-systems">
|
||||
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).</p>
|
||||
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
|
||||
<p>This package will read Photos databases for any supported version on any supported macOS version.
|
||||
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
|
||||
<p>Requires python >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
||||
</section>
|
||||
<section id="installation">
|
||||
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline">¶</a></h2>
|
||||
<p>If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.</p>
|
||||
<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>
|
||||
</section>
|
||||
<section id="installation-using-pip">
|
||||
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline">¶</a></h3>
|
||||
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="installation-from-git-repository">
|
||||
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline">¶</a></h3>
|
||||
<p>OSXPhotos uses setuptools, thus simply run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
|
||||
<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>
|
||||
</section>
|
||||
</section>
|
||||
<section id="command-line-usage">
|
||||
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h2>
|
||||
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
|
||||
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
|
||||
<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>
|
||||
<span class="n">repl</span> <span class="n">Run</span> <span class="n">interactive</span> <span class="n">osxphotos</span> <span class="n">shell</span>
|
||||
<span class="n">tutorial</span> <span class="n">Display</span> <span class="n">osxphotos</span> <span class="n">tutorial</span><span class="o">.</span>
|
||||
</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>
|
||||
<section id="command-line-examples">
|
||||
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline">¶</a></h3>
|
||||
<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>
|
||||
</section>
|
||||
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
||||
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">>results.json</span></code></p>
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
|
||||
<h4>export photos to file structure based on 4-digit year and full name of month of photo’s creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{created.month}"</span></code></p>
|
||||
<p>(by default, it will attempt to use the system library)</p>
|
||||
</section>
|
||||
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
|
||||
<h4>export photos to file structure based on 4-digit year of photo’s creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{media_type}"</span></code></p>
|
||||
</section>
|
||||
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
|
||||
<h4>export default library using ‘country name/year’ as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
||||
</section>
|
||||
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
||||
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline">¶</a></h4>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">"Big</span> <span class="pre">Videos"</span></code></p>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section id="example-uses-of-the-package">
|
||||
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Simple usage of the package """</span>
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
<section id="package-interface">
|
||||
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline">¶</a></h2>
|
||||
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
|
||||
<div class="toctree-wrapper compound">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></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>
|
||||
</section>
|
||||
</section>
|
||||
<section id="indices-and-tables">
|
||||
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
|
||||
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
</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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</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>
|
||||
107
docs/modules.html
Normal file
107
docs/modules.html
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||
|
||||
<title>osxphotos — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
<section id="osxphotos">
|
||||
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||
<div class="toctree-wrapper compound">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</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>
|
||||
6
docs/objects.inv
Normal file
6
docs/objects.inv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Sphinx inventory version 2
|
||||
# Project: osxphotos
|
||||
# Version:
|
||||
# The remainder of this file is compressed using zlib.
|
||||
xÚ…<EFBFBD>¿
|
||||
Â0‡÷<ŹéPÁÕÕ© PppŽÉÙ“»Ò¤ ›¯áëù$¶MµÜ’ßýò}ä”5àƒÞjV<6A>l@Y³.‚³ÀþV؃bç$i°†¬.R!,wût%r$CoÃÊ3Ú–òI#*m<>b¨õªQã„¶± ¼¯ÇÓCSR<>Á0-„cýÃVÞ“Ï â¬k‹½×u?6÷Ñì£bÄù#Èî¡`šz*¼`…Ôlfdú†ó¥–R]eŽÂ£¬T1ÕÅ,¾8ÆyÖvßžM™Å
|
||||
BIN
docs/osxphotos.pdf
Normal file
BIN
docs/osxphotos.pdf
Normal file
Binary file not shown.
113
docs/reference.html
Normal file
113
docs/reference.html
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||
|
||||
<title>osxphotos package — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="prev" 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">
|
||||
|
||||
<section id="osxphotos-package">
|
||||
<h1>osxphotos package<a class="headerlink" href="#osxphotos-package" title="Permalink to this headline">¶</a></h1>
|
||||
<section id="osxphotos-module">
|
||||
<h2>osxphotos module<a class="headerlink" href="#osxphotos-module" title="Permalink to this headline">¶</a></h2>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
</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 class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">osxphotos package</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#osxphotos-module">osxphotos module</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="index.html">Documentation overview</a><ul>
|
||||
<li>Previous: <a href="cli.html" title="previous 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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/reference.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
123
docs/search.html
Normal file
123
docs/search.html
Normal file
@@ -0,0 +1,123 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.44.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<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>
|
||||
|
||||
<noscript>
|
||||
<div class="admonition warning">
|
||||
<p>
|
||||
Please activate JavaScript to enable the search
|
||||
functionality.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
|
||||
<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="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<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 4.3.1</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
docs/searchindex.js
Normal file
1
docs/searchindex.js
Normal file
File diff suppressed because one or more lines are too long
367
docs/tutorial.html
Normal file
367
docs/tutorial.html
Normal file
@@ -0,0 +1,367 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Export your photos — osxphotos 0.42.20 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">
|
||||
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:START -->
|
||||
# OSXPhotos Tutorial
|
||||
|
||||
## Tutorial
|
||||
<!-- OSXPHOTOS-TUTORIAL-HEADER:END --><p>The design philosophy for osxphotos is “make the easy things easy and make the hard things possible”. To “make the hard things possible”, osxphotos is very flexible and has many, many configuration options – the <code class="docutils literal notranslate"><span class="pre">export</span></code> command for example, has over 100 command line options. Thus, osxphotos may seem daunting at first. The purpose of this tutorial is to explain a number of common use cases with examples and, hopefully, make osxphotos less daunting to use. osxphotos includes several commands for retrieving information from your Photos library but the one most users are interested in is the <code class="docutils literal notranslate"><span class="pre">export</span></code> command which exports photos from the library so that’s the focus of this tutorial.</p>
|
||||
<div class="section" id="export-your-photos">
|
||||
<h1>Export your photos<a class="headerlink" href="#export-your-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span></code></p>
|
||||
<p>This command exports all your photos to the <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code> directory.</p>
|
||||
<p><strong>Note</strong>: osxphotos uses the term ‘photo’ to refer to a generic media asset in your Photos Library. A photo may be an image, a video file, a combination of still image and video file (e.g. an Apple “Live Photo” which is an image and an associated “live preview” video file), a JPEG image with an associated RAW image, etc.</p>
|
||||
</div>
|
||||
<div class="section" id="export-by-date">
|
||||
<h1>Export by date<a class="headerlink" href="#export-by-date" title="Permalink to this headline">¶</a></h1>
|
||||
<p>While the previous command will export all your photos (and videos–see note above), it probably doesn’t do exactly what you want. In the previous example, all the photos will be exported to a single folder: <code class="docutils literal notranslate"><span class="pre">/path/to/export</span></code>. If you have a large library with thousands of images and videos, this likely isn’t very useful. You can use the <code class="docutils literal notranslate"><span class="pre">--export-by-date</span></code> option to export photos to a folder structure organized by year, month, day, e.g. <code class="docutils literal notranslate"><span class="pre">2021/04/21</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--export-by-date</span></code></p>
|
||||
<p>With this command, a photo that was created on 31 May 2015 would be exported to: <code class="docutils literal notranslate"><span class="pre">/path/to/export/2015/05/31</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="specify-directory-structure">
|
||||
<h1>Specify directory structure<a class="headerlink" href="#specify-directory-structure" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you prefer a different directory structure for your exported images, osxphotos provides a very flexible <span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:START --></span>template system<span class="raw-html-m2r"><!-- OSXPHOTOS-TEMPLATE-SYSTEM-LINK:END --></span> that allows you to specify the directory structure using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option. For example, this command exported to a directory structure that looks like: <code class="docutils literal notranslate"><span class="pre">2015/May</span></code> (4-digit year / month name):</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{created.month}"</span></code></p>
|
||||
<p>The string following <code class="docutils literal notranslate"><span class="pre">--directory</span></code> is an <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">template</span> <span class="pre">string</span></code>. Template strings are widely used throughout osxphotos and it’s worth your time to learn more about them. In a template string, the values between the curly braces, e.g. <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> are replaced with metadata from the photo being exported. In this case, <code class="docutils literal notranslate"><span class="pre">{created.year}</span></code> is the 4-digit year of the photo’s creation date and <code class="docutils literal notranslate"><span class="pre">{created.month}</span></code> is the full month name in the user’s locale (e.g. <code class="docutils literal notranslate"><span class="pre">May</span></code>, <code class="docutils literal notranslate"><span class="pre">mai</span></code>, etc.). In the osxphotos template system these are referred to as template fields. The text not included between <code class="docutils literal notranslate"><span class="pre">{}</span></code> pairs is interpreted literally, in this case <code class="docutils literal notranslate"><span class="pre">/</span></code>, is a directory separator.</p>
|
||||
<p>osxphotos provides access to almost all the metadata known to Photos about your images. For example, Photos performs reverse geolocation lookup on photos that contain GPS coordinates to assign place names to the photo. Using the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> template, you could thus export photos organized by country name:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{place.name.country}"</span></code></p>
|
||||
<p>Of course, some photos might not have an associated place name so the template system allows you specify a default value to use if a template field is null (has no value).</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{created.year}/{place.name.country,No-Country}"</span></code></p>
|
||||
<p>The value after the ‘,’ in the template string is the default value, in this case ‘No-Country’. <strong>Note</strong>: If you don’t specify a default value and a template field is null, osxphotos will use “_” (underscore character) as the default.</p>
|
||||
<p>Some template fields, such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code>, may expand to more than one value. For example, if a photo has keywords of “Travel” and “Vacation”, <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> would expand to “Travel”, “Vacation”. When used with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, this would result in the photo being exported to more than one directory (thus more than one copy of the photo would be exported). For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--directory</span> <span class="pre">"{keyword}"</span></code></p>
|
||||
<p>the exported files would be:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">/</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="specify-exported-filename">
|
||||
<h1>Specify exported filename<a class="headerlink" href="#specify-exported-filename" title="Permalink to this headline">¶</a></h1>
|
||||
<p>By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> or <code class="docutils literal notranslate"><span class="pre">DSC05678.dng</span></code>. osxphotos allows you to specify a custom filename template using the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option in the same way as <code class="docutils literal notranslate"><span class="pre">--directory</span></code> allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">"{title}"</span></code></p>
|
||||
<p>The above command will export photos using the title. Note that you don’t need to specify the extension as part of the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename "{title,{original_name}}"
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
Use photo's title as the filename <──────┘ ││ │
|
||||
││ │
|
||||
Value after comma will be used <───────┘│ │
|
||||
if title is blank │ │
|
||||
│ │
|
||||
The default value can be <────┘ │
|
||||
another template field │
|
||||
│
|
||||
Use photo's original name if no title <──────┘
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The osxphotos template system also allows for limited conditional logic of the type “If a condition is true then do one thing, otherwise, do a different thing”. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--filename</span></code> option to name files that are marked as “Favorites” in Photos differently than other files. For example, to add a “#” to the name of every photo that’s a favorite:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --filename "{original_name}{favorite?#,}"
|
||||
│ │ │││
|
||||
│ │ │││
|
||||
Use photo's original name as filename <──┘ │ │││
|
||||
│ │││
|
||||
'favorite' is True if photo is a Favorite, <───────┘ │││
|
||||
otherwise, False │││
|
||||
│││
|
||||
'?' specifies a conditional <─────────────┘││
|
||||
││
|
||||
Value immediately following ? will be used if <──────┘│
|
||||
preceding template field is True or non-blank │
|
||||
│
|
||||
Value immediately following comma will be used if <──────┘
|
||||
template field is False or blank (null); in this case
|
||||
no value is specified so a blank string "" will be used
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Like with <code class="docutils literal notranslate"><span class="pre">--directory</span></code>, using a multi-valued template field such as <code class="docutils literal notranslate"><span class="pre">{keyword}</span></code> may result in more than one copy of a photo being exported. For example, if <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> has keywords <code class="docutils literal notranslate"><span class="pre">Travel</span></code>, and <code class="docutils literal notranslate"><span class="pre">Vacation</span></code> and you run the following command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--filename</span> <span class="pre">"{keyword}-{original_name}"</span></code></p>
|
||||
<p>the exported files would be:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Travel</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
<span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">export</span><span class="o">/</span><span class="n">Vacation</span><span class="o">-</span><span class="n">IMG_1234</span><span class="o">.</span><span class="n">JPG</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="edited-photos">
|
||||
<h1>Edited photos<a class="headerlink" href="#edited-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If a photo has been edited in Photos (e.g. cropped, adjusted, etc.) there will be both an original image and an edited image in the Photos Library. By default, osxphotos will export both the original and the edited image. To distinguish between them, osxphotos will append “_edited” to the edited image. For example, if the original image was named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code>, osxphotos will export the original as <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the edited version as <code class="docutils literal notranslate"><span class="pre">IMG_1234_edited.jpeg</span></code>. <strong>Note:</strong> Photos changes the extension of edited images to “.jpeg” even if the original was named “.JPG”. You can change the suffix appended to edited images using the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">"_EDIT"</span></code></p>
|
||||
<p>In this example, the edited image would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234_EDIT.jpeg</span></code>. Like many options in osxphotos, the <code class="docutils literal notranslate"><span class="pre">--edited-suffix</span></code> option can evaluate an osxphotos template string so you could append the modification date (the date the photo was edited) to all edited photos using this command:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--edited-suffix</span> <span class="pre">"_{modified.year}-{modified.mm}-{modified.dd}"</span></code></p>
|
||||
<p>In this example, if the photo was edited on 21 April 2021, the name of the exported file would be: <code class="docutils literal notranslate"><span class="pre">IMG_1234_2021-04-21.jpeg</span></code>.</p>
|
||||
<p>You can tell osxphotos to not export edited photos (that is, only export the original unedited photos) using <code class="docutils literal notranslate"><span class="pre">--skip-edited</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-edited</span></code></p>
|
||||
<p>You can also tell osxphotos to export either the original photo (if the photo has not been edited) or the edited photo (if it has been edited), but not both, using the <code class="docutils literal notranslate"><span class="pre">--skip-original-if-edited</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--skip-original-if-edited</span></code></p>
|
||||
<p>As mentioned above, Photos renames JPEG images that have been edited with the “.jpeg” extension. Some applications use “.JPG” and others use “.jpg” or “.JPEG”. You can use the <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span></code> option to have osxphotos rename all JPEG files with the same extension. Valid values are jpeg, jpg, JPEG, JPG; e.g. <code class="docutils literal notranslate"><span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code> to use ‘.jpg’ for all JPEGs.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--jpeg-ext</span> <span class="pre">jpg</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="specifying-the-photos-library">
|
||||
<h1>Specifying the Photos library<a class="headerlink" href="#specifying-the-photos-library" title="Permalink to this headline">¶</a></h1>
|
||||
<p>All the above commands operate on the default Photos library. Most users only use a single Photos library which is also known as the System Photo Library. It is possible to use Photos with more than one library. For example, if you hold down the “Option” key while opening Photos, you can select an alternate Photos library. If you don’t specify which library to use, osxphotos will try find the last opened library. Occasionally it can’t determine this and in that case, it will use the System Photos Library. If you use more than one Photos library and want to explicitly specify which library to use, you can do so with the <code class="docutils literal notranslate"><span class="pre">--db</span></code> option. (db is short for database and is so named because osxphotos operates on the database that Photos uses to manage your Photos library).</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--db</span> <span class="pre">~/Pictures/MyAlternateLibrary.photoslibrary</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="missing-photos">
|
||||
<h1>Missing photos<a class="headerlink" href="#missing-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn’t yet synched the cloud library to the local Mac or you have Photos configured to “Optimize Mac Storage” in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac.</p>
|
||||
<p>If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> option. <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code> uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos’ AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There’s also an experimental <code class="docutils literal notranslate"><span class="pre">--use-photokit</span></code> option that will communicate with Photos using a different “PhotoKit” interface. This option must be used together with <code class="docutils literal notranslate"><span class="pre">--download-missing</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span></code></p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--download-missing</span> <span class="pre">--use-photokit</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="exporting-to-external-disks">
|
||||
<h1>Exporting to external disks<a class="headerlink" href="#exporting-to-external-disks" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you are exporting to an external network attached storage (NAS) device, you may encounter errors if the network connection is unreliable. In this case, you can use the <code class="docutils literal notranslate"><span class="pre">--retry</span></code> option so that osxphotos will automatically retry the export. Use <code class="docutils literal notranslate"><span class="pre">--retry</span></code> with a number that specifies the number of times to retry the export:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--retry</span> <span class="pre">3</span></code></p>
|
||||
<p>In this example, osxphotos will attempt to export a photo up to 3 times if it encounters an error.</p>
|
||||
</div>
|
||||
<div class="section" id="exporting-metadata-with-exported-photos">
|
||||
<h1>Exporting metadata with exported photos<a class="headerlink" href="#exporting-metadata-with-exported-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Photos tracks a tremendous amount of metadata associated with photos in the library such as keywords, faces and persons, reverse geolocation data, and image classification labels. Photos’ native export capability does not preserve most of this metadata. osxphotos can, however, access and preserve almost all the metadata associated with photos. Using the free <cite>``exiftool`</cite> <<a class="reference external" href="https://exiftool.org/">https://exiftool.org/</a>>`_ app, osxphotos can write metadata to exported photos. Follow the instructions on the exiftool website to install exiftool then you can use the <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> option to write metadata to exported photos:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span></code></p>
|
||||
<p>This will write basic metadata such as keywords, persons, and GPS location to the exported files. osxphotos includes several additional options that can be used in conjunction with <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify the metadata that is written by <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>. For example, you can use the <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> option to specify custom keywords (again, via the osxphotos template system). For example, to use the folder and album a photo is in to create hierarchal keywords in the format used by Lightroom Classic:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}"
|
||||
│ │
|
||||
│ │
|
||||
folder_album results in the folder(s) <──┘ │
|
||||
and album a photo is contained in │
|
||||
│
|
||||
The value in () is used as the path separator <───────┘
|
||||
for joining the folders and albums. For example,
|
||||
if photo is in Folder1/Folder2/Album, (>) produces
|
||||
"Folder1>Folder2>Album" which some programs, such as
|
||||
Lightroom Classic, treat as hierarchal keywords
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The above command will write all the regular metadata that <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form “Folder1>Folder2>Album”. If you did not include the <code class="docutils literal notranslate"><span class="pre">(>)</span></code> in the template string (e.g. <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>), folder_album would render in form “Folder1/Folder2/Album”.</p>
|
||||
<p>A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the <code class="docutils literal notranslate"><span class="pre">{label}</span></code>. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the image’s metadata:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span></code></p>
|
||||
<p><strong>Note</strong>: When evaluating templates for <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code>, osxphotos inserts the automatic default value “_” for any template field which is null (empty or blank). This is to ensure that there’s never a null directory or filename created. For metadata templates such as <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code>, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add “nolabel” as a keyword for any photo that doesn’t have labels:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label,nolabel}"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="sidecar-files">
|
||||
<h1>Sidecar files<a class="headerlink" href="#sidecar-files" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Another way to export metadata about your photos is through the use of sidecar files. These are files that have the same name as your photo (but with a different extension) and carry the metadata. Many digital asset management applications (for example, PhotoPrism, Lightroom, Digikam, etc.) can read or write sidecar files. osxphotos can export metadata in exiftool compatible JSON and XMP formats using the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> option. For example, to output metadata to XMP sidecars:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span></code></p>
|
||||
<p>Unlike <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code>, you do not need to install exiftool to use the <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code> feature. Many of the same configuration options that apply to <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> to modify metadata, for example, <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> can also be used with <code class="docutils literal notranslate"><span class="pre">--sidecar</span></code>.</p>
|
||||
<p>Sidecar files are named “photoname.ext.sidecar_ext”. For example, if the photo is named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG</span></code> and the sidecar format is XMP, the sidecar would be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.JPG.XMP</span></code>. Some applications expect the sidecar in this case to be named <code class="docutils literal notranslate"><span class="pre">IMG_1234.XMP</span></code>. You can use the <code class="docutils literal notranslate"><span class="pre">-sidecar-drop-ext</span></code> option to force osxphotos to name the sidecar files in this manner:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--sidecar</span> <span class="pre">XMP</span> <span class="pre">-sidecar-drop-ext</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="updating-a-previous-export">
|
||||
<h1>Updating a previous export<a class="headerlink" href="#updating-a-previous-export" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you want to use osxphotos to perform periodic backups of your Photos library rather than a one-time export, use the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option. When <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> is run, it creates a database file named <code class="docutils literal notranslate"><span class="pre">.osxphotos_export.db</span></code> in the export folder. (<strong>Note</strong> Because the filename starts with a “.”, you won’t see it in Finder which treats “dot-files” like this as hidden. You will see the file in the Terminal.) . If you run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option, it will look for this database file and, if found, use it to retrieve state information from the last time it was run to only export new or changed files. For example:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span></code></p>
|
||||
<p>will read the export database located in <code class="docutils literal notranslate"><span class="pre">/path/to/export/.osxphotos_export.db</span></code> and only export photos that have been added or changed since the last time osxphotos was run. You can run osxphotos with the <code class="docutils literal notranslate"><span class="pre">--update</span></code> option even if it’s never been run before. If the database isn’t found, osxphotos will create it. If you run <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> without <code class="docutils literal notranslate"><span class="pre">--update</span></code> in a folder where you had previously exported photos, it will re-export all the photos. If your intent is to keep a periodic backup of your Photos Library up to date with osxphotos, you should always use <code class="docutils literal notranslate"><span class="pre">--update</span></code>.</p>
|
||||
<p>If your workflow involves moving files out of the export directory (for example, you move them into a digital asset management app) but you want to use the features of <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you can use the <code class="docutils literal notranslate"><span class="pre">--only-new</span></code> with <code class="docutils literal notranslate"><span class="pre">--update</span></code> to force osxphotos to only export photos that are new (added to the library) since the last update. In this case, osxphotos will ignore the previously exported files that are now missing. Without <code class="docutils literal notranslate"><span class="pre">--only-new</span></code>, osxphotos would see that previously exported files are missing and re-export them.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--only-new</span></code></p>
|
||||
<p>If your workflow involves editing the images you exported from Photos but you still want to maintain a backup with <code class="docutils literal notranslate"><span class="pre">--update</span></code>, you should use the <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> option. <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code> instructs osxphotos to ignore the file’s signature (for example, size and date modified) when deciding which files should be updated with <code class="docutils literal notranslate"><span class="pre">--update</span></code>. If you edit a file in the export directory and then run <code class="docutils literal notranslate"><span class="pre">--update</span></code> without <code class="docutils literal notranslate"><span class="pre">--ignore-signature</span></code>, osxphotos will see that the file is different than the one in the Photos library and re-export it.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--update</span> <span class="pre">--ignore-signature</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="dry-run">
|
||||
<h1>Dry Run<a class="headerlink" href="#dry-run" title="Permalink to this headline">¶</a></h1>
|
||||
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--dry-run</span></code> option to have osxphotos “dry run” or test an export without actually exporting any files. When combined with the <code class="docutils literal notranslate"><span class="pre">--verbose</span></code> option, which causes osxphotos to print out details of every file being exported, this can be a useful tool for testing your export options before actually running a full export. For example, if you are learning the template system and want to verify that your <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code> templates are correct, <code class="docutils literal notranslate"><span class="pre">--dry-run</span> <span class="pre">--verbose</span></code> will print out the name of each file being exported.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--dry-run</span> <span class="pre">--verbose</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="creating-a-report-of-all-exported-files">
|
||||
<h1>Creating a report of all exported files<a class="headerlink" href="#creating-a-report-of-all-exported-files" title="Permalink to this headline">¶</a></h1>
|
||||
<p>You can use the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option to create a report, in comma-separated values (CSV) format that will list the details of all files that were exported, skipped, missing, etc. This file format is compatible with programs such as Microsoft Excel. Provide the name of the report after the <code class="docutils literal notranslate"><span class="pre">--report</span></code> option:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--report</span> <span class="pre">export.csv</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="exporting-only-certain-photos">
|
||||
<h1>Exporting only certain photos<a class="headerlink" href="#exporting-only-certain-photos" title="Permalink to this headline">¶</a></h1>
|
||||
<p>By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of “query options” that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them–read the help text (<code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">export</span></code>) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.</p>
|
||||
<p>For example, to export only photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">"Travel"</span></code></p>
|
||||
<p>Like many options in osxphotos, <code class="docutils literal notranslate"><span class="pre">--keyword</span></code> (and most other query options) can be repeated to search for more than one term. For example, to find photos with keyword <code class="docutils literal notranslate"><span class="pre">Travel</span></code> <em>or</em> keyword <code class="docutils literal notranslate"><span class="pre">Vacation</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--keyword</span> <span class="pre">"Travel"</span> <span class="pre">--keyword</span> <span class="pre">"Vacation"</span></code></p>
|
||||
<p>To export only photos contained in the album “Summer Vacation”:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--album</span> <span class="pre">"Summer</span> <span class="pre">Vacation"</span></code></p>
|
||||
<p>There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone “Portrait Mode”:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--portrait</span></code></p>
|
||||
<p>You can also export photos in a certain date range:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--from-date</span> <span class="pre">"2020-01-01"</span> <span class="pre">--to-date</span> <span class="pre">"2020-02-28"</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="converting-images-to-jpeg-on-export">
|
||||
<h1>Converting images to JPEG on export<a class="headerlink" href="#converting-images-to-jpeg-on-export" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Photos can store images in many different formats. osxphotos can convert non-JPEG images (for example, RAW photos) to JPEG on export using the <code class="docutils literal notranslate"><span class="pre">--convert-to-jpeg</span></code> option. You can specify the JPEG quality (0: worst, 1.0: best) using <code class="docutils literal notranslate"><span class="pre">--jpeg-quality</span></code>. For example:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--convert-to-jpeg</span> <span class="pre">--jpeg-quality</span> <span class="pre">0.9</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="finder-attributes">
|
||||
<h1>Finder attributes<a class="headerlink" href="#finder-attributes" title="Permalink to this headline">¶</a></h1>
|
||||
<p>In addition to using <code class="docutils literal notranslate"><span class="pre">exiftool</span></code> to write metadata directly to the image metadata, osxphotos can write certain metadata that is available to the Finder and Spotlight but does not modify the actual image file. This is done through something called extended attributes which are stored in the filesystem with a file but do not actually modify the file itself. Finder tags and Finder comments are common examples of these.</p>
|
||||
<p>osxphotos can, for example, write any keywords in the image to Finder tags so that you can search for images in Spotlight or the Finder using the <code class="docutils literal notranslate"><span class="pre">tag:tagname</span></code> syntax:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span></code></p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">--finder-tag-keywords</span></code> also works with <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code> as described above in the section on <code class="docutils literal notranslate"><span class="pre">exiftool</span></code>:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span></code></p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--xattr-template</span></code> option allows you to set a variety of other extended attributes. It is used in the format <code class="docutils literal notranslate"><span class="pre">--xattr-template</span> <span class="pre">ATTRIBUTE</span> <span class="pre">TEMPLATE</span></code> where ATTRIBUTE is one of ‘authors’,’comment’, ‘copyright’, ‘description’, ‘findercomment’, ‘headline’, ‘keywords’.</p>
|
||||
<p>For example, to set Finder comment to the photo’s title and description:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{newline}{descr}"</span></code></p>
|
||||
<p>In the template string above, <code class="docutils literal notranslate"><span class="pre">{newline}</span></code> instructs osxphotos to insert a new line character (“n”) between the title and description. In this example, if <code class="docutils literal notranslate"><span class="pre">{title}</span></code> or <code class="docutils literal notranslate"><span class="pre">{descr}</span></code> is empty, you’ll get “titlen” or “ndescription” which may not be desired so you can use more advanced features of the template system to handle these cases:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{title?{descr?{newline},},}{descr}"</span></code></p>
|
||||
<p>Explanation of the template string:</p>
|
||||
<div class="highlight-txt notranslate"><div class="highlight"><pre><span></span>{title}{title?{descr?{newline},},}{descr}
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──> insert title │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└───> is there a title?
|
||||
│ │ │ │ │
|
||||
└───> if so, is there a description?
|
||||
│ │ │ │
|
||||
└───> if so, insert new line
|
||||
│ │ │
|
||||
└───> if descr is blank, insert nothing
|
||||
│ │
|
||||
└───> if title is blank, insert nothing
|
||||
│
|
||||
└───> finally, insert description
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>In this example, <code class="docutils literal notranslate"><span class="pre">title?</span></code> demonstrates use of the boolean (True/False) feature of the template system. <code class="docutils literal notranslate"><span class="pre">title?</span></code> is read as “Is the title True (or not blank/empty)? If so, then the value immediately following the <code class="docutils literal notranslate"><span class="pre">?</span></code> is used in place of <code class="docutils literal notranslate"><span class="pre">title</span></code>. If <code class="docutils literal notranslate"><span class="pre">title</span></code> is blank, then the value immediately following the comma is used instead. The format for boolean fields is <code class="docutils literal notranslate"><span class="pre">field?value</span> <span class="pre">if</span> <span class="pre">true,value</span> <span class="pre">if</span> <span class="pre">false</span></code>. Either <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">true</span></code> or <code class="docutils literal notranslate"><span class="pre">value</span> <span class="pre">if</span> <span class="pre">false</span></code> may be blank, in which case a blank string (“”) is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex <code class="docutils literal notranslate"><span class="pre">if-then-else</span></code> statements.</p>
|
||||
<p>The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.</p>
|
||||
<p>See Extended Attributes section in the help for <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span></code> for additional information about this feature.</p>
|
||||
</div>
|
||||
<div class="section" id="saving-and-loading-options">
|
||||
<h1>Saving and loading options<a class="headerlink" href="#saving-and-loading-options" title="Permalink to this headline">¶</a></h1>
|
||||
<p>If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (<code class="docutils literal notranslate"><span class="pre">--save-config</span> <span class="pre">FILE</span></code>) and then load them (<code class="docutils literal notranslate"><span class="pre">--load-config</span> <span class="pre">FILE</span></code>) instead of repeating each option on the command line.</p>
|
||||
<p>To save the configuration:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre"><all</span> <span class="pre">your</span> <span class="pre">options</span> <span class="pre">here></span> <span class="pre">--update</span> <span class="pre">--save-config</span> <span class="pre">osxphotos.toml</span></code></p>
|
||||
<p>Then the next to you run osxphotos, you can simply do this:</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--load-config</span> <span class="pre">osxphotos.toml</span></code></p>
|
||||
<p>The configuration file is a plain text file in <a class="reference external" href="https://toml.io/en/">TOML</a> format so the <code class="docutils literal notranslate"><span class="pre">.toml</span></code> extension is standard but you can name the file anything you like.</p>
|
||||
</div>
|
||||
<div class="section" id="an-example-from-an-actual-osxphotos-user">
|
||||
<h1>An example from an actual osxphotos user<a class="headerlink" href="#an-example-from-an-actual-osxphotos-user" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Here’s a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>I usually import my iPhone’s photo roll on a more or less regular basis, and it
|
||||
includes photos and videos. As a result, the size ot my Photos library may rise
|
||||
very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
|
||||
quite good keyword management system.
|
||||
|
||||
After a while, I want to take most of the videos out of the library and move them
|
||||
to a separate "videos" folder on a different folder / volume. As I might want to
|
||||
use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
|
||||
its internal library tagging system, I will use osxphotos to do just this.
|
||||
|
||||
Picking the videos can be left to Photos, using a smart folder for instance. Then
|
||||
just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
|
||||
to spot all videos created on my iPhone using the Quik application (now part of
|
||||
GoPro).
|
||||
|
||||
I want to retrieve my keywords only and make sure they populate the Finder tags, as
|
||||
well as export all the persons identified in the videos by Photos. I also want to
|
||||
merge any keywords or persons already in the video metadata with the exported
|
||||
metadata.
|
||||
|
||||
Keeping Photo’s edited titles and descriptions and putting both in the Finder
|
||||
comments field in a readable manner is also enabled.
|
||||
|
||||
And I want to keep the file’s creation date (using `--touch-file`).
|
||||
|
||||
Finally, use `--strip` to remove any leading or trailing whitespace from processed
|
||||
template fields.
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/folder</span> <span class="pre">for</span> <span class="pre">exported</span> <span class="pre">videos/</span> <span class="pre">--keyword</span> <span class="pre">Quik</span> <span class="pre">--only-movies</span> <span class="pre">--db</span> <span class="pre">/path</span> <span class="pre">to</span> <span class="pre">my.photoslibrary</span> <span class="pre">--touch-file</span> <span class="pre">--finder-tag-keywords</span> <span class="pre">--person-keyword</span> <span class="pre">--xattr-template</span> <span class="pre">findercomment</span> <span class="pre">"{title}{title?{descr?{newline},},}{descr}"</span> <span class="pre">--exiftool-merge-keywords</span> <span class="pre">--exiftool-merge-persons</span> <span class="pre">--exiftool</span> <span class="pre">--strip</span></code></p>
|
||||
</div>
|
||||
<div class="section" id="conclusion">
|
||||
<h1>Conclusion<a class="headerlink" href="#conclusion" title="Permalink to this headline">¶</a></h1>
|
||||
<p>osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the <code class="docutils literal notranslate"><span class="pre">--directory</span></code> option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.</p>
|
||||
</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.5.2</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
<a href="_sources/tutorial.md.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
28
docsrc/Makefile
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
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
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
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
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
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
5
docsrc/source/modules.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
osxphotos
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
13
docsrc/source/reference.rst
Normal file
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:
|
||||
112
examples/album_sort_order.py
Normal file
112
examples/album_sort_order.py
Normal file
@@ -0,0 +1,112 @@
|
||||
""" Example function for use with osxphotos export --post-function option showing how to record album sort order """
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from typing import Optional
|
||||
|
||||
from osxphotos import ExportResults, PhotoInfo
|
||||
from osxphotos.albuminfo import AlbumInfo
|
||||
from osxphotos.path_utils import sanitize_dirname
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
|
||||
|
||||
def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
|
||||
"""Call this with {function} template to get album sequence (sort order) when exporting with {folder_album} template
|
||||
|
||||
For example, calling this template function like the following prepends sequence#_ to each exported file if the file is in an album:
|
||||
|
||||
osxphotos export /path/to/export -V --directory "{folder_album}" --filename "{album?{function:examples/album_sort_order.py::album_sequence}_,}{original_name}"
|
||||
|
||||
The sequence will start at 0. To change the sequence to start at a different offset (e.g. 1), set the environment variable OSXPHOTOS_ALBUM_SEQUENCE_START=1 (or whatever offset you want)
|
||||
"""
|
||||
dest_path = options.dest_path
|
||||
if not dest_path:
|
||||
return ""
|
||||
|
||||
album_info = None
|
||||
for album in photo.album_info:
|
||||
# following code is how {folder_album} builds the folder path
|
||||
folder = "/".join(sanitize_dirname(f) for f in album.folder_names)
|
||||
folder += "/" + sanitize_dirname(album.title)
|
||||
if dest_path.endswith(folder):
|
||||
album_info = album
|
||||
break
|
||||
else:
|
||||
# didn't find the album, so skip this file
|
||||
return ""
|
||||
start_index = int(os.getenv("OSXPHOTOS_ALBUM_SEQUENCE_START", 0))
|
||||
return str(album_info.photo_index(photo) + start_index)
|
||||
|
||||
|
||||
def album_sort_order(
|
||||
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
|
||||
):
|
||||
"""Call this with osxphotos export /path/to/export --post-function album_sort_order.py::album_sort_order
|
||||
This will get called immediately after the photo has been exported
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance for the photo that's just been exported
|
||||
results: ExportResults instance with information about the files associated with the exported photo
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
Will not be called if --dry-run flag is enabled
|
||||
Will be called immediately after export and before any --post-command commands are executed
|
||||
"""
|
||||
|
||||
# ExportResults has the following properties
|
||||
# fields with filenames contain the full path to the file
|
||||
# exported: list of all files exported
|
||||
# new: list of all new files exported (--update)
|
||||
# updated: list of all files updated (--update)
|
||||
# skipped: list of all files skipped (--update)
|
||||
# exif_updated: list of all files that were updated with --exiftool
|
||||
# touched: list of all files that had date updated with --touch-file
|
||||
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
|
||||
# sidecar_json_written: list of all JSON sidecar files written
|
||||
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
|
||||
# sidecar_exiftool_written: list of all exiftool sidecar files written
|
||||
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
|
||||
# sidecar_xmp_written: list of all XMP sidecar files written
|
||||
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
|
||||
# missing: list of all missing files
|
||||
# error: list tuples of (filename, error) for any errors generated during export
|
||||
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
|
||||
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
|
||||
# xattr_written: list of files that had extended attributes written
|
||||
# xattr_skipped: list of files that where extended attributes were skipped (--update)
|
||||
# deleted_files: list of deleted files
|
||||
# deleted_directories: list of deleted directories
|
||||
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
|
||||
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
|
||||
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
|
||||
|
||||
for filepath in results.exported:
|
||||
# do your processing here
|
||||
filepath = pathlib.Path(filepath)
|
||||
album_dir = filepath.parent.name
|
||||
if album_dir not in photo.albums:
|
||||
return
|
||||
|
||||
# get the first album that matches this name of which the photo is a member
|
||||
album_info = None
|
||||
for album in photo.album_info:
|
||||
if album.title == album_dir:
|
||||
album_info = album
|
||||
break
|
||||
else:
|
||||
# didn't find the album, so skip this file
|
||||
return
|
||||
|
||||
try:
|
||||
sort_order = album_info.photo_index(photo)
|
||||
except ValueError:
|
||||
# photo not in album, so skip this file
|
||||
return
|
||||
|
||||
verbose(f"Sort order for {filepath} in album {album_dir} is {sort_order}")
|
||||
with open(str(filepath) + "_sort_order.txt", "w") as f:
|
||||
f.write(str(sort_order))
|
||||
173
examples/export_template.py
Normal file
173
examples/export_template.py
Normal file
@@ -0,0 +1,173 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template
|
||||
to export photos in a folder structure similar to Photos' own structure
|
||||
|
||||
Use: osxphotos export /path/to/export --directory "{function:/path/to/export_template.py::photos_folders}"
|
||||
|
||||
This will likely export multiple copies of each photo. If using APFS file system, this should be
|
||||
a non-issue as osxphotos will use copy-on-write so each exported photo doesn't take up additional space
|
||||
unless you edit the photo.
|
||||
|
||||
Thank-you @mkirkland4874 for the inspiration for this example!
|
||||
|
||||
This will produce output similar to this:
|
||||
|
||||
Library
|
||||
- Photos
|
||||
-- {created.year}
|
||||
---- {created.mm}
|
||||
------ {created.dd}
|
||||
- Favorites
|
||||
- Hidden
|
||||
- Recently Deleted
|
||||
- People
|
||||
- Places
|
||||
- Imports
|
||||
Media Types
|
||||
- Videos
|
||||
- Selfies
|
||||
- Portrait
|
||||
- Panoramas
|
||||
- Time-lapse
|
||||
- Slow-mo
|
||||
- Bursts
|
||||
- Screenshots
|
||||
My Albums
|
||||
-- Album 1
|
||||
-- Album 2
|
||||
-- Folder 1
|
||||
---- Album 3
|
||||
Shared Albums
|
||||
-- Shared Album 1
|
||||
-- Shared Album 2
|
||||
"""
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||
from osxphotos.path_utils import sanitize_dirname
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
|
||||
|
||||
def place_folder(photo: osxphotos.PhotoInfo) -> str:
|
||||
"""Return places as folder in format Country/State/City/etc."""
|
||||
if not photo.place:
|
||||
return ""
|
||||
|
||||
places = []
|
||||
if photo.place.names.country:
|
||||
places.append(photo.place.names.country[0])
|
||||
|
||||
if photo.place.names.state_province:
|
||||
places.append(photo.place.names.state_province[0])
|
||||
|
||||
if photo.place.names.sub_administrative_area:
|
||||
places.append(photo.place.names.sub_administrative_area[0])
|
||||
|
||||
if photo.place.names.additional_city_info:
|
||||
places.append(photo.place.names.additional_city_info[0])
|
||||
|
||||
if photo.place.names.area_of_interest:
|
||||
places.append(photo.place.names.area_of_interest[0])
|
||||
|
||||
if places:
|
||||
return "Library/Places/" + "/".join(sanitize_dirname(place) for place in places)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def photos_folders(photo: osxphotos.PhotoInfo, options: osxphotos.phototemplate.RenderOptions, **kwargs) -> Union[List, str]:
|
||||
"""template function for use with --directory to export photos in a folder structure similar to Photos
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: RenderOptions instance
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns: list of directories for each photo
|
||||
|
||||
"""
|
||||
|
||||
rendered_date, _ = photo.render_template("{created.year}/{created.mm}/{created.dd}")
|
||||
date_path = rendered_date[0]
|
||||
|
||||
def add_date_path(path):
|
||||
"""add date path (year/mm/dd)"""
|
||||
return f"{path}/{date_path}"
|
||||
|
||||
# Library
|
||||
|
||||
directories = []
|
||||
if not photo.hidden and not photo.intrash and not photo.shared:
|
||||
# set directories to [Library/Photos/year/mm/dd]
|
||||
# render_template returns a tuple of [rendered value(s)], [unmatched]
|
||||
# here, we can ignore the unmatched value, assigned to _, as we know template will match
|
||||
directories, _ = photo.render_template(
|
||||
"Library/Photos/{created.year}/{created.mm}/{created.dd}"
|
||||
)
|
||||
|
||||
if photo.favorite:
|
||||
directories.append(add_date_path("Library/Favorites"))
|
||||
if photo.hidden:
|
||||
directories.append(add_date_path("Library/Hidden"))
|
||||
if photo.intrash:
|
||||
directories.append(add_date_path("Library/Recently Deleted"))
|
||||
|
||||
directories.extend(
|
||||
[
|
||||
add_date_path(f"Library/People/{person}")
|
||||
for person in photo.persons
|
||||
if person != _UNKNOWN_PERSON
|
||||
]
|
||||
)
|
||||
|
||||
if photo.place:
|
||||
directories.append(add_date_path(place_folder(photo)))
|
||||
|
||||
if photo.import_info:
|
||||
dt = DateTimeFormatter(photo.import_info.creation_date)
|
||||
directories.append(f"Library/Imports/{dt.year}/{dt.mm}/{dt.dd}")
|
||||
|
||||
# Media Types
|
||||
|
||||
if photo.ismovie:
|
||||
directories.append(add_date_path("Media Types/Videos"))
|
||||
if photo.selfie:
|
||||
directories.append(add_date_path("Media Types/Selfies"))
|
||||
if photo.live_photo:
|
||||
directories.append(add_date_path("Media Types/Live Photos"))
|
||||
if photo.portrait:
|
||||
directories.append(add_date_path("Media Types/Portrait"))
|
||||
if photo.panorama:
|
||||
directories.append(add_date_path("Media Types/Panoramas"))
|
||||
if photo.time_lapse:
|
||||
directories.append(add_date_path("Media Types/Time-lapse"))
|
||||
if photo.slow_mo:
|
||||
directories.append(add_date_path("Media Types/Slo-mo"))
|
||||
if photo.burst:
|
||||
directories.append(add_date_path("Media Types/Bursts"))
|
||||
if photo.screenshot:
|
||||
directories.append(add_date_path("Media Types/Screenshots"))
|
||||
|
||||
# Albums
|
||||
|
||||
# render the folders and albums in folder/subfolder/album format
|
||||
# the __NO_ALBUM__ is used as a sentinel to strip out photos not in an album
|
||||
# use RenderOptions.dirname to force the rendered folder_album value to be sanitized as a valid path
|
||||
# use RenderOptions.none_str to specify custom value for any photo that doesn't belong to an album so
|
||||
# those can be filtered out; if not specified, none_str is "_"
|
||||
folder_albums, _ = photo.render_template(
|
||||
"{folder_album}", RenderOptions(dirname=True, none_str="__NO_ALBUM__")
|
||||
)
|
||||
|
||||
root_directory = "Shared Albums/" if photo.shared else "My Albums/"
|
||||
directories.extend(
|
||||
[
|
||||
root_directory + folder_album
|
||||
for folder_album in folder_albums
|
||||
if folder_album != "__NO_ALBUM__"
|
||||
]
|
||||
)
|
||||
|
||||
return directories
|
||||
@@ -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):
|
||||
|
||||
54
examples/post_function.py
Normal file
54
examples/post_function.py
Normal file
@@ -0,0 +1,54 @@
|
||||
""" Example function for use with osxphotos export --post-function option """
|
||||
|
||||
from osxphotos import PhotoInfo, ExportResults
|
||||
|
||||
|
||||
def post_function(
|
||||
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
|
||||
):
|
||||
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
|
||||
This will get called immediately after the photo has been exported
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance for the photo that's just been exported
|
||||
results: ExportResults instance with information about the files associated with the exported photo
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
Will not be called if --dry-run flag is enabled
|
||||
Will be called immediately after export and before any --post-command commands are executed
|
||||
"""
|
||||
|
||||
# ExportResults has the following properties
|
||||
# fields with filenames contain the full path to the file
|
||||
# exported: list of all files exported
|
||||
# new: list of all new files exported (--update)
|
||||
# updated: list of all files updated (--update)
|
||||
# skipped: list of all files skipped (--update)
|
||||
# exif_updated: list of all files that were updated with --exiftool
|
||||
# touched: list of all files that had date updated with --touch-file
|
||||
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
|
||||
# sidecar_json_written: list of all JSON sidecar files written
|
||||
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
|
||||
# sidecar_exiftool_written: list of all exiftool sidecar files written
|
||||
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
|
||||
# sidecar_xmp_written: list of all XMP sidecar files written
|
||||
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
|
||||
# missing: list of all missing files
|
||||
# error: list tuples of (filename, error) for any errors generated during export
|
||||
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
|
||||
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
|
||||
# xattr_written: list of files that had extended attributes written
|
||||
# xattr_skipped: list of files that where extended attributes were skipped (--update)
|
||||
# deleted_files: list of deleted files
|
||||
# deleted_directories: list of deleted directories
|
||||
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
|
||||
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
|
||||
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
|
||||
|
||||
for filename in results.exported:
|
||||
# do your processing here
|
||||
verbose(f"post_function: {photo.original_filename} exported as {filename}")
|
||||
31
examples/query_function.py
Normal file
31
examples/query_function.py
Normal file
@@ -0,0 +1,31 @@
|
||||
""" example function for osxphotos --query-function """
|
||||
|
||||
from typing import List
|
||||
|
||||
from osxphotos import PhotoInfo
|
||||
|
||||
|
||||
# call this with --query-function examples/query_function.py::best_selfies
|
||||
def best_selfies(photos: List[PhotoInfo]) -> List[PhotoInfo]:
|
||||
"""your query function should take a list of PhotoInfo objects and return a list of PhotoInfo objects (or empty list)"""
|
||||
# this example finds your best selfie for every year
|
||||
|
||||
# get list of selfies sorted by date
|
||||
photos = sorted([p for p in photos if p.selfie], key=lambda p: p.date)
|
||||
if not photos:
|
||||
return []
|
||||
|
||||
start_year = photos[0].date.year
|
||||
stop_year = photos[-1].date.year
|
||||
best_selfies = []
|
||||
for year in range(start_year, stop_year + 1):
|
||||
# find best selfie each year as determined by overall aesthetic score
|
||||
selfies = sorted(
|
||||
[p for p in photos if p.date.year == year],
|
||||
key=lambda p: p.score.overall,
|
||||
reverse=True,
|
||||
)
|
||||
if selfies:
|
||||
best_selfies.append(selfies[0])
|
||||
|
||||
return best_selfies
|
||||
17
examples/template_filter.py
Normal file
17
examples/template_filter.py
Normal file
@@ -0,0 +1,17 @@
|
||||
""" Example of using a custom python function as an osxphotos template filter
|
||||
|
||||
Use in formath:
|
||||
"{template_field|template_filter.py::myfilter}"
|
||||
|
||||
Your filter function will receive a list of strings even if the template renders to a single value.
|
||||
You should expect a list and return a list and be able to handle multi-value templates like {keyword}
|
||||
as well as single-value templates like {original_name}
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
def myfilter(values: List[str]) -> List[str]:
|
||||
""" Custom filter to append "foo-" to template value """
|
||||
values = ["foo-" + val for val in values]
|
||||
return values
|
||||
|
||||
30
examples/template_function.py
Normal file
30
examples/template_function.py
Normal file
@@ -0,0 +1,30 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template
|
||||
Use: osxphotos export /path/to/export --filename "{function:/path/to/template_function.py::example}"
|
||||
|
||||
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
from typing import List, Union
|
||||
|
||||
import osxphotos
|
||||
|
||||
|
||||
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
||||
""" example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
filename = pathlib.Path(photo.original_filename).stem
|
||||
if photo.hasadjustments:
|
||||
filename += "#"
|
||||
if photo.favorite:
|
||||
filename += "!"
|
||||
|
||||
return filename
|
||||
@@ -8,41 +8,50 @@ import importlib
|
||||
pathex = os.getcwd()
|
||||
|
||||
# include necessary data files
|
||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
|
||||
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||
datas = [
|
||||
("osxphotos/templates/xmp_sidecar.mako", "osxphotos/templates"),
|
||||
("osxphotos/templates/xmp_sidecar_beta.mako", "osxphotos/templates"),
|
||||
("osxphotos/phototemplate.tx", "osxphotos"),
|
||||
("osxphotos/phototemplate.md", "osxphotos"),
|
||||
("osxphotos/tutorial.md", "osxphotos"),
|
||||
]
|
||||
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
||||
for package, files in package_imports:
|
||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||
datas.extend((os.path.join(proot, f), package) for f in files)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(['cli.py'],
|
||||
pathex=[pathex],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=['pkg_resources.py2_warn'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
a = Analysis(
|
||||
["cli.py"],
|
||||
pathex=[pathex],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=["pkg_resources.py2_warn"],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='osxphotos',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True )
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name="osxphotos",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from ._constants import AlbumSortOrder
|
||||
from ._version import __version__
|
||||
from .photoinfo import PhotoInfo
|
||||
from .exiftool import ExifTool
|
||||
from .photoinfo import ExportResults, PhotoInfo
|
||||
from .photosdb import PhotosDB
|
||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||
from .phototemplate import PhotoTemplate
|
||||
from .queryoptions import QueryOptions
|
||||
from .utils import _debug, _get_logger, _set_debug
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
# TODO: Add test for __str__ and to_json
|
||||
# TODO: Add special albums and magic albums
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
||||
""" applescript -- Easy-to-use Python wrapper for NSAppleScript """
|
||||
|
||||
import sys
|
||||
|
||||
from Foundation import NSAppleScript, NSAppleEventDescriptor, NSURL, \
|
||||
NSAppleScriptErrorMessage, NSAppleScriptErrorBriefMessage, \
|
||||
NSAppleScriptErrorNumber, NSAppleScriptErrorAppName, NSAppleScriptErrorRange
|
||||
|
||||
from .aecodecs import Codecs, fourcharcode, AEType, AEEnum
|
||||
from . import kae
|
||||
|
||||
__all__ = ['AppleScript', 'ScriptError', 'AEType', 'AEEnum', 'kMissingValue', 'kae']
|
||||
|
||||
|
||||
######################################################################
|
||||
|
||||
|
||||
class AppleScript:
|
||||
""" Represents a compiled AppleScript. The script object is persistent; its handlers may be called multiple times and its top-level properties will retain current state until the script object's disposal.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
_codecs = Codecs()
|
||||
|
||||
def __init__(self, source=None, path=None):
|
||||
"""
|
||||
source : str | None -- AppleScript source code
|
||||
path : str | None -- full path to .scpt/.applescript file
|
||||
|
||||
Notes:
|
||||
|
||||
- Either the path or the source argument must be provided.
|
||||
|
||||
- If the script cannot be read/compiled, a ScriptError is raised.
|
||||
"""
|
||||
if path:
|
||||
url = NSURL.fileURLWithPath_(path)
|
||||
self._script, errorinfo = NSAppleScript.alloc().initWithContentsOfURL_error_(url, None)
|
||||
if errorinfo:
|
||||
raise ScriptError(errorinfo)
|
||||
elif source:
|
||||
self._script = NSAppleScript.alloc().initWithSource_(source)
|
||||
else:
|
||||
raise ValueError("Missing source or path argument.")
|
||||
if not self._script.isCompiled():
|
||||
errorinfo = self._script.compileAndReturnError_(None)[1]
|
||||
if errorinfo:
|
||||
raise ScriptError(errorinfo)
|
||||
|
||||
def __repr__(self):
|
||||
s = self.source
|
||||
return 'AppleScript({})'.format(repr(s) if len(s) < 100 else '{}...{}'.format(repr(s)[:80], repr(s)[-17:]))
|
||||
|
||||
##
|
||||
|
||||
def _newevent(self, suite, code, args):
|
||||
evt = NSAppleEventDescriptor.appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID_(
|
||||
fourcharcode(suite), fourcharcode(code), NSAppleEventDescriptor.nullDescriptor(), 0, 0)
|
||||
evt.setDescriptor_forKeyword_(self._codecs.pack(args), fourcharcode(kae.keyDirectObject))
|
||||
return evt
|
||||
|
||||
def _unpackresult(self, result, errorinfo):
|
||||
if not result:
|
||||
raise ScriptError(errorinfo)
|
||||
return self._codecs.unpack(result)
|
||||
|
||||
##
|
||||
|
||||
source = property(lambda self: str(self._script.source()), doc="str -- the script's source code")
|
||||
|
||||
def run(self, *args):
|
||||
""" Run the script, optionally passing arguments to its run handler.
|
||||
|
||||
args : anything -- arguments to pass to script, if any; see also supported type mappings documentation
|
||||
Result : anything | None -- the script's return value, if any
|
||||
|
||||
Notes:
|
||||
|
||||
- The run handler must be explicitly declared in order to pass arguments.
|
||||
|
||||
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
|
||||
|
||||
- If execution fails, a ScriptError is raised.
|
||||
"""
|
||||
if args:
|
||||
evt = self._newevent(kae.kCoreEventClass, kae.kAEOpenApplication, args)
|
||||
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
|
||||
else:
|
||||
return self._unpackresult(*self._script.executeAndReturnError_(None))
|
||||
|
||||
def call(self, name, *args):
|
||||
""" Call the specified user-defined handler.
|
||||
|
||||
name : str -- the handler's name (case-sensitive)
|
||||
args : anything -- arguments to pass to script, if any; see documentation for supported types
|
||||
Result : anything | None -- the script's return value, if any
|
||||
|
||||
Notes:
|
||||
|
||||
- The handler's name must be a user-defined identifier, not an AppleScript keyword; e.g. 'myCount' is acceptable; 'count' is not.
|
||||
|
||||
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
|
||||
|
||||
- If execution fails, a ScriptError is raised.
|
||||
"""
|
||||
evt = self._newevent(kae.kASAppleScriptSuite, kae.kASPrepositionalSubroutine, args)
|
||||
evt.setDescriptor_forKeyword_(NSAppleEventDescriptor.descriptorWithString_(name),
|
||||
fourcharcode(kae.keyASSubroutineName))
|
||||
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
|
||||
|
||||
|
||||
##
|
||||
|
||||
|
||||
class ScriptError(Exception):
|
||||
""" Indicates an AppleScript compilation/execution error. """
|
||||
|
||||
def __init__(self, errorinfo):
|
||||
self._errorinfo = dict(errorinfo)
|
||||
|
||||
def __repr__(self):
|
||||
return 'ScriptError({})'.format(self._errorinfo)
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
""" str -- the error message """
|
||||
msg = self._errorinfo.get(NSAppleScriptErrorMessage)
|
||||
if not msg:
|
||||
msg = self._errorinfo.get(NSAppleScriptErrorBriefMessage, 'Script Error')
|
||||
return msg
|
||||
|
||||
number = property(lambda self: self._errorinfo.get(NSAppleScriptErrorNumber),
|
||||
doc="int | None -- the error number, if given")
|
||||
|
||||
appname = property(lambda self: self._errorinfo.get(NSAppleScriptErrorAppName),
|
||||
doc="str | None -- the name of the application that reported the error, where relevant")
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
""" (int, int) -- the start and end points (1-indexed) within the source code where the error occurred """
|
||||
range = self._errorinfo.get(NSAppleScriptErrorRange)
|
||||
if range:
|
||||
start = range.rangeValue().location
|
||||
end = start + range.rangeValue().length
|
||||
return (start, end)
|
||||
else:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
msg = self.message
|
||||
for s, v in [(' ({})', self.number), (' app={!r}', self.appname), (' range={0[0]}-{0[1]}', self.range)]:
|
||||
if v is not None:
|
||||
msg += s.format(v)
|
||||
return msg.encode('ascii', 'replace') if sys.version_info.major < 3 else msg # 2.7 compatibility
|
||||
|
||||
|
||||
##
|
||||
|
||||
|
||||
kMissingValue = AEType(kae.cMissingValue) # convenience constant
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
""" aecodecs -- Convert from common Python types to Apple Event Manager types and vice-versa. """
|
||||
|
||||
import datetime, struct, sys
|
||||
|
||||
from Foundation import NSAppleEventDescriptor, NSURL
|
||||
|
||||
from . import kae
|
||||
|
||||
|
||||
__all__ = ['Codecs', 'AEType', 'AEEnum']
|
||||
|
||||
|
||||
######################################################################
|
||||
|
||||
|
||||
def fourcharcode(code):
|
||||
""" Convert four-char code for use in NSAppleEventDescriptor methods.
|
||||
|
||||
code : bytes -- four-char code, e.g. b'utxt'
|
||||
Result : int -- OSType, e.g. 1970567284
|
||||
"""
|
||||
return struct.unpack('>I', code)[0]
|
||||
|
||||
|
||||
#######
|
||||
|
||||
|
||||
class Codecs:
|
||||
""" Implements mappings for common Python types with direct AppleScript equivalents. Used by AppleScript class. """
|
||||
|
||||
kMacEpoch = datetime.datetime(1904, 1, 1)
|
||||
kUSRF = fourcharcode(kae.keyASUserRecordFields)
|
||||
|
||||
def __init__(self):
|
||||
# Clients may add/remove/replace encoder and decoder items:
|
||||
self.encoders = {
|
||||
NSAppleEventDescriptor.class__(): self.packdesc,
|
||||
type(None): self.packnone,
|
||||
bool: self.packbool,
|
||||
int: self.packint,
|
||||
float: self.packfloat,
|
||||
bytes: self.packbytes,
|
||||
str: self.packstr,
|
||||
list: self.packlist,
|
||||
tuple: self.packlist,
|
||||
dict: self.packdict,
|
||||
datetime.datetime: self.packdatetime,
|
||||
AEType: self.packtype,
|
||||
AEEnum: self.packenum,
|
||||
}
|
||||
if sys.version_info.major < 3: # 2.7 compatibility
|
||||
self.encoders[unicode] = self.packstr
|
||||
|
||||
self.decoders = {fourcharcode(k): v for k, v in {
|
||||
kae.typeNull: self.unpacknull,
|
||||
kae.typeBoolean: self.unpackboolean,
|
||||
kae.typeFalse: self.unpackboolean,
|
||||
kae.typeTrue: self.unpackboolean,
|
||||
kae.typeSInt32: self.unpacksint32,
|
||||
kae.typeIEEE64BitFloatingPoint: self.unpackfloat64,
|
||||
kae.typeUTF8Text: self.unpackunicodetext,
|
||||
kae.typeUTF16ExternalRepresentation: self.unpackunicodetext,
|
||||
kae.typeUnicodeText: self.unpackunicodetext,
|
||||
kae.typeLongDateTime: self.unpacklongdatetime,
|
||||
kae.typeAEList: self.unpackaelist,
|
||||
kae.typeAERecord: self.unpackaerecord,
|
||||
kae.typeAlias: self.unpackfile,
|
||||
kae.typeFSS: self.unpackfile,
|
||||
kae.typeFSRef: self.unpackfile,
|
||||
kae.typeFileURL: self.unpackfile,
|
||||
kae.typeType: self.unpacktype,
|
||||
kae.typeEnumeration: self.unpackenumeration,
|
||||
}.items()}
|
||||
|
||||
def pack(self, data):
|
||||
"""Pack Python data.
|
||||
data : anything -- a Python value
|
||||
Result : NSAppleEventDescriptor -- an AE descriptor, or error if no encoder exists for this type of data
|
||||
"""
|
||||
try:
|
||||
return self.encoders[data.__class__](data) # quick lookup by type/class
|
||||
except (KeyError, AttributeError) as e:
|
||||
for type, encoder in self.encoders.items(): # slower but more thorough lookup that can handle subtypes/subclasses
|
||||
if isinstance(data, type):
|
||||
return encoder(data)
|
||||
raise TypeError("Can't pack data into an AEDesc (unsupported type): {!r}".format(data))
|
||||
|
||||
def unpack(self, desc):
|
||||
"""Unpack an Apple event descriptor.
|
||||
desc : NSAppleEventDescriptor
|
||||
Result : anything -- a Python value, or the original NSAppleEventDescriptor if no decoder is found
|
||||
"""
|
||||
decoder = self.decoders.get(desc.descriptorType())
|
||||
# unpack known type
|
||||
if decoder:
|
||||
return decoder(desc)
|
||||
# if it's a record-like desc, unpack as dict with an extra AEType(b'pcls') key containing the desc type
|
||||
rec = desc.coerceToDescriptorType_(fourcharcode(kae.typeAERecord))
|
||||
if rec:
|
||||
rec = self.unpackaerecord(rec)
|
||||
rec[AEType(kae.pClass)] = AEType(struct.pack('>I', desc.descriptorType()))
|
||||
return rec
|
||||
# return as-is
|
||||
return desc
|
||||
|
||||
##
|
||||
|
||||
def _packbytes(self, desctype, data):
|
||||
return NSAppleEventDescriptor.descriptorWithDescriptorType_bytes_length_(
|
||||
fourcharcode(desctype), data, len(data))
|
||||
|
||||
def packdesc(self, val):
|
||||
return val
|
||||
|
||||
def packnone(self, val):
|
||||
return NSAppleEventDescriptor.nullDescriptor()
|
||||
|
||||
def packbool(self, val):
|
||||
return NSAppleEventDescriptor.descriptorWithBoolean_(int(val))
|
||||
|
||||
def packint(self, val):
|
||||
if (-2**31) <= val < (2**31):
|
||||
return NSAppleEventDescriptor.descriptorWithInt32_(val)
|
||||
else:
|
||||
return self.pack(float(val))
|
||||
|
||||
def packfloat(self, val):
|
||||
return self._packbytes(kae.typeFloat, struct.pack('d', val))
|
||||
|
||||
def packbytes(self, val):
|
||||
return self._packbytes(kae.typeData, val)
|
||||
|
||||
def packstr(self, val):
|
||||
return NSAppleEventDescriptor.descriptorWithString_(val)
|
||||
|
||||
def packdatetime(self, val):
|
||||
delta = val - self.kMacEpoch
|
||||
sec = delta.days * 3600 * 24 + delta.seconds
|
||||
return self._packbytes(kae.typeLongDateTime, struct.pack('q', sec))
|
||||
|
||||
def packlist(self, val):
|
||||
lst = NSAppleEventDescriptor.listDescriptor()
|
||||
for item in val:
|
||||
lst.insertDescriptor_atIndex_(self.pack(item), 0)
|
||||
return lst
|
||||
|
||||
def packdict(self, val):
|
||||
record = NSAppleEventDescriptor.recordDescriptor()
|
||||
usrf = desctype = None
|
||||
for key, value in val.items():
|
||||
if isinstance(key, AEType):
|
||||
if key.code == kae.pClass and isinstance(value, AEType): # AS packs records that contain a 'class' property by coercing the packed record to the descriptor type specified by the property's value (assuming it's an AEType)
|
||||
desctype = value
|
||||
else:
|
||||
record.setDescriptor_forKeyword_(self.pack(value), fourcharcode(key.code))
|
||||
else:
|
||||
if not usrf:
|
||||
usrf = NSAppleEventDescriptor.listDescriptor()
|
||||
usrf.insertDescriptor_atIndex_(self.pack(key), 0)
|
||||
usrf.insertDescriptor_atIndex_(self.pack(value), 0)
|
||||
if usrf:
|
||||
record.setDescriptor_forKeyword_(usrf, self.kUSRF)
|
||||
if desctype:
|
||||
newrecord = record.coerceToDescriptorType_(fourcharcode(desctype.code))
|
||||
if newrecord:
|
||||
record = newrecord
|
||||
else: # coercion failed for some reason, so pack as normal key-value pair
|
||||
record.setDescriptor_forKeyword_(self.pack(desctype), fourcharcode(key.code))
|
||||
return record
|
||||
|
||||
def packtype(self, val):
|
||||
return NSAppleEventDescriptor.descriptorWithTypeCode_(fourcharcode(val.code))
|
||||
|
||||
def packenum(self, val):
|
||||
return NSAppleEventDescriptor.descriptorWithEnumCode_(fourcharcode(val.code))
|
||||
|
||||
#######
|
||||
|
||||
def unpacknull(self, desc):
|
||||
return None
|
||||
|
||||
def unpackboolean(self, desc):
|
||||
return desc.booleanValue()
|
||||
|
||||
def unpacksint32(self, desc):
|
||||
return desc.int32Value()
|
||||
|
||||
def unpackfloat64(self, desc):
|
||||
return struct.unpack('d', bytes(desc.data()))[0]
|
||||
|
||||
def unpackunicodetext(self, desc):
|
||||
return desc.stringValue()
|
||||
|
||||
def unpacklongdatetime(self, desc):
|
||||
return self.kMacEpoch + datetime.timedelta(seconds=struct.unpack('q', bytes(desc.data()))[0])
|
||||
|
||||
def unpackaelist(self, desc):
|
||||
return [self.unpack(desc.descriptorAtIndex_(i + 1)) for i in range(desc.numberOfItems())]
|
||||
|
||||
def unpackaerecord(self, desc):
|
||||
dct = {}
|
||||
for i in range(desc.numberOfItems()):
|
||||
key = desc.keywordForDescriptorAtIndex_(i + 1)
|
||||
value = desc.descriptorForKeyword_(key)
|
||||
if key == self.kUSRF:
|
||||
lst = self.unpackaelist(value)
|
||||
for i in range(0, len(lst), 2):
|
||||
dct[lst[i]] = lst[i+1]
|
||||
else:
|
||||
dct[AEType(struct.pack('>I', key))] = self.unpack(value)
|
||||
return dct
|
||||
|
||||
def unpacktype(self, desc):
|
||||
return AEType(struct.pack('>I', desc.typeCodeValue()))
|
||||
|
||||
def unpackenumeration(self, desc):
|
||||
return AEEnum(struct.pack('>I', desc.enumCodeValue()))
|
||||
|
||||
def unpackfile(self, desc):
|
||||
url = bytes(desc.coerceToDescriptorType_(fourcharcode(kae.typeFileURL)).data()).decode('utf8')
|
||||
return NSURL.URLWithString_(url).path()
|
||||
|
||||
|
||||
#######
|
||||
|
||||
|
||||
class AETypeBase:
|
||||
""" Base class for AEType and AEEnum.
|
||||
|
||||
Notes:
|
||||
|
||||
- Hashable and comparable, so may be used as keys in dictionaries that map to AE records.
|
||||
"""
|
||||
|
||||
def __init__(self, code):
|
||||
"""
|
||||
code : bytes -- four-char code, e.g. b'utxt'
|
||||
"""
|
||||
if not isinstance(code, bytes):
|
||||
raise TypeError('invalid code (not a bytes object): {!r}'.format(code))
|
||||
elif len(code) != 4:
|
||||
raise ValueError('invalid code (not four bytes long): {!r}'.format(code))
|
||||
self._code = code
|
||||
|
||||
code = property(lambda self:self._code, doc="bytes -- four-char code, e.g. b'utxt'")
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._code)
|
||||
|
||||
def __eq__(self, val):
|
||||
return val.__class__ == self.__class__ and val.code == self._code
|
||||
|
||||
def __ne__(self, val):
|
||||
return not self == val
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({!r})".format(self.__class__.__name__, self._code)
|
||||
|
||||
|
||||
##
|
||||
|
||||
|
||||
class AEType(AETypeBase):
|
||||
"""An AE type. Maps to an AppleScript type class, e.g. AEType(b'utxt') <=> 'unicode text'."""
|
||||
|
||||
|
||||
class AEEnum(AETypeBase):
|
||||
"""An AE enumeration. Maps to an AppleScript constant, e.g. AEEnum(b'yes ') <=> 'yes'."""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ Constants used by osxphotos
|
||||
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
@@ -29,16 +30,19 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
||||
# Photos 6 (10.16.0 Beta) == 14104
|
||||
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
|
||||
|
||||
_PHOTOS_2_VERSION = "2622"
|
||||
|
||||
# only version 3 - 4 have RKVersion.selfPortrait
|
||||
_PHOTOS_3_VERSION = "3301"
|
||||
|
||||
# versions 5.0 and later have a different database structure
|
||||
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
|
||||
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.6
|
||||
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.7 (also Big Sur and Monterey which switch to model version)
|
||||
|
||||
# Ranges for model version by Photos version
|
||||
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
|
||||
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
|
||||
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134
|
||||
|
||||
# some table names differ between Photos 5 and Photos 6
|
||||
_DB_TABLE_NAMES = {
|
||||
@@ -49,6 +53,10 @@ _DB_TABLE_NAMES = {
|
||||
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
|
||||
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
|
||||
"DEPTH_STATE": "ZGENERICASSET.ZDEPTHSTATES",
|
||||
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
|
||||
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
|
||||
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
|
||||
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
|
||||
},
|
||||
6: {
|
||||
"ASSET": "ZASSET",
|
||||
@@ -57,6 +65,22 @@ _DB_TABLE_NAMES = {
|
||||
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
|
||||
"IMPORT_FOK": "null",
|
||||
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
||||
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
|
||||
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
|
||||
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
|
||||
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
|
||||
},
|
||||
7: {
|
||||
"ASSET": "ZASSET",
|
||||
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_38KEYWORDS",
|
||||
"ALBUM_JOIN": "Z_27ASSETS.Z_3ASSETS",
|
||||
"ALBUM_SORT_ORDER": "Z_27ASSETS.Z_FOK_3ASSETS",
|
||||
"IMPORT_FOK": "null",
|
||||
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
||||
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
|
||||
"ASSET_ALBUM_JOIN": "Z_27ASSETS.Z_27ALBUMS",
|
||||
"ASSET_ALBUM_TABLE": "Z_27ASSETS",
|
||||
"HDR_TYPE": "ZHDRTYPE",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -69,6 +93,11 @@ _TESTED_OS_VERSIONS = [
|
||||
("10", "16"),
|
||||
("11", "0"),
|
||||
("11", "1"),
|
||||
("11", "2"),
|
||||
("11", "3"),
|
||||
("11", "4"),
|
||||
("11", "5"),
|
||||
("11", "6"),
|
||||
]
|
||||
|
||||
# Photos 5 has persons who are empty string if unidentified face
|
||||
@@ -94,12 +123,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||
# Constants used for processing folders and albums
|
||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
|
||||
_PHOTOS_5_PROJECT_ALBUM_KIND = 1508 # My Projects (e.g. Calendar, Card, Slideshow)
|
||||
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
||||
|
||||
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
||||
_PHOTOS_4_ALBUM_TYPE_ALBUM = 1 # RKAlbum.albumType
|
||||
_PHOTOS_4_ALBUM_TYPE_PROJECT = 9 # RKAlbum.albumType
|
||||
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW = 8 # RKAlbum.albumType
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS = [
|
||||
"TopLevelAlbums",
|
||||
"TopLevelKeepsakes",
|
||||
"TopLevelSlideshows",
|
||||
]
|
||||
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
|
||||
|
||||
# EXIF related constants
|
||||
@@ -186,6 +223,9 @@ DEFAULT_EDITED_SUFFIX = "_edited"
|
||||
# Default suffix to add to original images
|
||||
DEFAULT_ORIGINAL_SUFFIX = ""
|
||||
|
||||
# Default suffix to add to preview images
|
||||
DEFAULT_PREVIEW_SUFFIX = "_preview"
|
||||
|
||||
# Colors for print CLI messages
|
||||
CLI_COLOR_ERROR = "red"
|
||||
CLI_COLOR_WARNING = "yellow"
|
||||
@@ -200,9 +240,62 @@ EXTENDED_ATTRIBUTE_NAMES = [
|
||||
"authors",
|
||||
"comment",
|
||||
"copyright",
|
||||
"creator",
|
||||
"description",
|
||||
"findercomment",
|
||||
"headline",
|
||||
"keywords",
|
||||
"participants",
|
||||
"projects",
|
||||
"rating",
|
||||
"subject",
|
||||
"title",
|
||||
"version",
|
||||
]
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
||||
|
||||
# name of export DB
|
||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||
|
||||
# bit flags for burst images ("burstPickType")
|
||||
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
|
||||
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
|
||||
BURST_SELECTED = 0b1000 # 8: burst image is selected
|
||||
BURST_KEY = 0b10000 # 16: burst image is the key photo (top of burst stack)
|
||||
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
|
||||
|
||||
LIVE_VIDEO_EXTENSIONS = [".mov"]
|
||||
|
||||
# categories that --post-command can be used with; these map to ExportResults fields
|
||||
POST_COMMAND_CATEGORIES = {
|
||||
"exported": "All exported files",
|
||||
"new": "When used with '--update', all newly exported files",
|
||||
"updated": "When used with '--update', all files which were previously exported but updated this time",
|
||||
"skipped": "When used with '--update', all files which were skipped (because they were previously exported and didn't change)",
|
||||
"missing": "All files which were not exported because they were missing from the Photos library",
|
||||
"exif_updated": "When used with '--exiftool', all files on which exiftool updated the metadata",
|
||||
"touched": "When used with '--touch-file', all files where the date was touched",
|
||||
"converted_to_jpeg": "When used with '--convert-to-jpeg', all files which were converted to jpeg",
|
||||
"sidecar_json_written": "When used with '--sidecar json', all JSON sidecar files which were written",
|
||||
"sidecar_json_skipped": "When used with '--sidecar json' and '--update', all JSON sidecar files which were skipped",
|
||||
"sidecar_exiftool_written": "When used with '--sidecar exiftool', all exiftool sidecar files which were written",
|
||||
"sidecar_exiftool_skipped": "When used with '--sidecar exiftool' and '--update, all exiftool sidecar files which were skipped",
|
||||
"sidecar_xmp_written": "When used with '--sidecar xmp', all XMP sidecar files which were written",
|
||||
"sidecar_xmp_skipped": "When used with '--sidecar xmp' and '--update', all XMP sidecar files which were skipped",
|
||||
"error": "All files which produced an error during export",
|
||||
# "deleted_files": "When used with '--cleanup', all files deleted during the export",
|
||||
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
||||
}
|
||||
|
||||
|
||||
class AlbumSortOrder(Enum):
|
||||
"""Album Sort Order"""
|
||||
|
||||
UNKNOWN = 0
|
||||
MANUAL = 1
|
||||
NEWEST_FIRST = 2
|
||||
OLDEST_FIRST = 3
|
||||
TITLE = 5
|
||||
|
||||
|
||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.40.2"
|
||||
__version__ = "0.44.2"
|
||||
|
||||
174
osxphotos/adjustmentsinfo.py
Normal file
174
osxphotos/adjustmentsinfo.py
Normal file
@@ -0,0 +1,174 @@
|
||||
""" AdjustmentsInfo class to read adjustments data for photos edited in Apple's Photos.app
|
||||
In Catalina and Big Sur, the adjustments data (data about edits done to the photo)
|
||||
is stored in a plist file in
|
||||
~/Pictures/Photos Library.photoslibrary/resources/renders/X/UUID.plist
|
||||
where X is first character of the photo's UUID string and UUID is the full UUID,
|
||||
e.g.: ~/Pictures/Photos Library.photoslibrary/resources/renders/3/30362C1D-192F-4CCD-9A2A-968F436DC0DE.plist
|
||||
|
||||
Thanks to @neilpa who figured out how to decode this information:
|
||||
Reference: https://github.com/neilpa/photohack/issues/4
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import plistlib
|
||||
import zlib
|
||||
|
||||
from .datetime_utils import datetime_naive_to_utc
|
||||
|
||||
|
||||
class AdjustmentsDecodeError(Exception):
|
||||
"""Could not decode adjustments plist file"""
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class AdjustmentsInfo:
|
||||
def __init__(self, plist_file):
|
||||
self._plist_file = plist_file
|
||||
self._plist = self._load_plist_file(plist_file)
|
||||
|
||||
self._base_version = self._plist.get("adjustmentBaseVersion", None)
|
||||
self._data = self._plist.get("adjustmentData", None)
|
||||
self._editor_bundle_id = self._plist.get("adjustmentEditorBundleID", None)
|
||||
self._format_identifier = self._plist.get("adjustmentFormatIdentifier", None)
|
||||
self._format_version = self._plist.get("adjustmentFormatVersion")
|
||||
self._timestamp = self._plist.get("adjustmentTimestamp", None)
|
||||
if self._timestamp and type(self._timestamp) == datetime.datetime:
|
||||
self._timestamp = datetime_naive_to_utc(self._timestamp)
|
||||
|
||||
try:
|
||||
self._adjustments = self._decode_adjustments_from_plist(self._plist)
|
||||
except Exception as e:
|
||||
self._adjustments = None
|
||||
|
||||
def _decode_adjustments_from_plist(self, plist):
|
||||
"""decode adjustmentData from Apple Photos adjustments
|
||||
|
||||
Args:
|
||||
plist: a plist dict as loaded by plistlib
|
||||
|
||||
Returns:
|
||||
decoded adjustmentsData as dict
|
||||
"""
|
||||
|
||||
return json.loads(
|
||||
zlib.decompress(plist["adjustmentData"], -zlib.MAX_WBITS).decode()
|
||||
)
|
||||
|
||||
def _load_plist_file(self, plist_file):
|
||||
"""Load plist file from disk
|
||||
|
||||
Args:
|
||||
plist_file: full path to plist file
|
||||
|
||||
Returns:
|
||||
plist as dict
|
||||
"""
|
||||
with open(str(plist_file), "rb") as fd:
|
||||
plist_dict = plistlib.load(fd)
|
||||
return plist_dict
|
||||
|
||||
@property
|
||||
def plist(self):
|
||||
"""The actual adjustments plist content as a dict """
|
||||
return self._plist
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""The raw adjustments data as a binary blob """
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def editor(self):
|
||||
"""The editor bundle ID for app/plug-in which made the adjustments """
|
||||
return self._editor_bundle_id
|
||||
|
||||
@property
|
||||
def format_id(self):
|
||||
"""The value of the adjustmentFormatIdentifier field in the plist """
|
||||
return self._format_identifier
|
||||
|
||||
@property
|
||||
def base_version(self):
|
||||
"""Value of adjustmentBaseVersion field """
|
||||
return self._base_version
|
||||
|
||||
@property
|
||||
def format_version(self):
|
||||
"""The value of the adjustmentFormatVersion in the plist """
|
||||
return self._format_version
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp """
|
||||
return self._timestamp
|
||||
|
||||
@property
|
||||
def adjustments(self):
|
||||
"""List of adjustment dictionaries (or empty list if none or could not be decoded)"""
|
||||
try:
|
||||
return self._adjustments["adjustments"] if self._adjustments else []
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
@property
|
||||
def adj_metadata(self):
|
||||
"""Metadata dictionary or None if adjustment data could not be decoded"""
|
||||
try:
|
||||
return self._adjustments["metadata"] if self._adjustments else None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def adj_orientation(self):
|
||||
"""EXIF orientation of image or 0 if none specified or None if adjustments could not be decoded"""
|
||||
try:
|
||||
return self._adjustments["metadata"]["orientation"]
|
||||
except KeyError:
|
||||
# no orientation field
|
||||
return 0
|
||||
except TypeError:
|
||||
# adjustments is None
|
||||
return 0
|
||||
|
||||
@property
|
||||
def adj_format_version(self):
|
||||
"""Format version for adjustments data (formatVersion field from adjustmentData) or None if adjustments could not be decoded"""
|
||||
try:
|
||||
return self._adjustments["formatVersion"] if self._adjustments else None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def adj_version_info(self):
|
||||
"""version info for adjustments data or None if adjustments data could not be decoded"""
|
||||
try:
|
||||
return self._adjustments["versionInfo"] if self._adjustments else None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def asdict(self):
|
||||
"""Returns all adjustments info as dictionary"""
|
||||
timestamp = self.timestamp
|
||||
if type(timestamp) == datetime.datetime:
|
||||
timestamp = timestamp.isoformat()
|
||||
|
||||
return {
|
||||
"data": self.data,
|
||||
"editor": self.editor,
|
||||
"format_id": self.format_id,
|
||||
"base_version": self.base_version,
|
||||
"format_version": self.format_version,
|
||||
"adjustments": self.adjustments,
|
||||
"metadata": self.adj_metadata,
|
||||
"orientation": self.adj_orientation,
|
||||
"adjustment_format_version": self.adj_format_version,
|
||||
"version_info": self.adj_version_info,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f"AdjustmentsInfo(plist_file='{self._plist_file}')"
|
||||
@@ -14,26 +14,28 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from ._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
TIME_DELTA,
|
||||
AlbumSortOrder,
|
||||
)
|
||||
from .datetime_utils import get_local_tz
|
||||
from .query_builder import get_query
|
||||
|
||||
|
||||
def sort_list_by_keys(values, sort_keys):
|
||||
""" Sorts list values by a second list sort_keys
|
||||
"""Sorts list values by a second list sort_keys
|
||||
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
|
||||
|
||||
Args:
|
||||
values: a list of values to be sorted
|
||||
sort_keys: a list of keys to sort values by
|
||||
|
||||
|
||||
Returns:
|
||||
list of values, sorted by sort_keys
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: raised if len(values) != len(sort_keys)
|
||||
"""
|
||||
@@ -63,12 +65,12 @@ class AlbumInfoBaseClass:
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" return uuid of album """
|
||||
"""return uuid of album"""
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def creation_date(self):
|
||||
""" return creation date of album """
|
||||
"""return creation date of album"""
|
||||
try:
|
||||
return self._creation_date
|
||||
except AttributeError:
|
||||
@@ -90,8 +92,8 @@ class AlbumInfoBaseClass:
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
""" For Albums, return start date (earliest image) of album or None for albums with no images
|
||||
For Import Sessions, return start date of import session (when import began) """
|
||||
"""For Albums, return start date (earliest image) of album or None for albums with no images
|
||||
For Import Sessions, return start date of import session (when import began)"""
|
||||
try:
|
||||
return self._start_date
|
||||
except AttributeError:
|
||||
@@ -109,8 +111,8 @@ class AlbumInfoBaseClass:
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
""" For Albums, return end date (most recent image) of album or None for albums with no images
|
||||
For Import Sessions, return end date of import sessions (when import was completed) """
|
||||
"""For Albums, return end date (most recent image) of album or None for albums with no images
|
||||
For Import Sessions, return end date of import sessions (when import was completed)"""
|
||||
try:
|
||||
return self._end_date
|
||||
except AttributeError:
|
||||
@@ -130,43 +132,74 @@ class AlbumInfoBaseClass:
|
||||
def photos(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._owner
|
||||
except AttributeError:
|
||||
try:
|
||||
personid = self._db._dbalbum_details[self.uuid][
|
||||
"cloudownerhashedpersonid"
|
||||
]
|
||||
self._owner = (
|
||||
self._db._db_hashed_person_id[personid]["full_name"]
|
||||
if personid
|
||||
else None
|
||||
)
|
||||
except KeyError:
|
||||
self._owner = None
|
||||
return self._owner
|
||||
|
||||
def __len__(self):
|
||||
""" return number of photos contained in album """
|
||||
"""return number of photos contained in album"""
|
||||
return len(self.photos)
|
||||
|
||||
|
||||
class AlbumInfo(AlbumInfoBaseClass):
|
||||
"""
|
||||
Base class for AlbumInfo, ImportInfo
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" return title / name of album """
|
||||
"""return title / name of album"""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in album sorted in same sort order as Photos """
|
||||
"""return list of photos contained in album sorted in same sort order as Photos"""
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
if self.uuid in self._db._dbalbums_album:
|
||||
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
|
||||
sorted_uuid = sort_list_by_keys(uuid, sort_order)
|
||||
self._photos = self._db.photos_by_uuid(sorted_uuid)
|
||||
photos = self._db.photos_by_uuid(sorted_uuid)
|
||||
sort_order = self.sort_order
|
||||
if sort_order == AlbumSortOrder.NEWEST_FIRST:
|
||||
self._photos = sorted(photos, key=lambda p: p.date, reverse=True)
|
||||
elif sort_order == AlbumSortOrder.OLDEST_FIRST:
|
||||
self._photos = sorted(photos, key=lambda p: p.date)
|
||||
elif sort_order == AlbumSortOrder.TITLE:
|
||||
self._photos = sorted(photos, key=lambda p: p.title or "")
|
||||
else:
|
||||
# assume AlbumSortOrder.MANUAL
|
||||
self._photos = photos
|
||||
else:
|
||||
self._photos = []
|
||||
return self._photos
|
||||
|
||||
@property
|
||||
def folder_names(self):
|
||||
""" return hierarchical list of folders the album is contained in
|
||||
the folder list is in form:
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders """
|
||||
"""return hierarchical list of folders the album is contained in
|
||||
the folder list is in form:
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders"""
|
||||
|
||||
try:
|
||||
return self._folder_names
|
||||
@@ -176,10 +209,10 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
|
||||
@property
|
||||
def folder_list(self):
|
||||
""" return hierarchical list of folders the album is contained in
|
||||
as list of FolderInfo objects in form
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders """
|
||||
"""return hierarchical list of folders the album is contained in
|
||||
as list of FolderInfo objects in form
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders"""
|
||||
|
||||
try:
|
||||
return self._folders
|
||||
@@ -189,7 +222,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """
|
||||
"""returns FolderInfo object for parent folder or None if no parent (e.g. top-level album)"""
|
||||
try:
|
||||
return self._parent
|
||||
except AttributeError:
|
||||
@@ -197,7 +230,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
else None
|
||||
)
|
||||
else:
|
||||
@@ -209,11 +242,43 @@ class AlbumInfo(AlbumInfoBaseClass):
|
||||
)
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def sort_order(self):
|
||||
"""return sort order of album"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return AlbumSortOrder.MANUAL
|
||||
|
||||
details = self._db._dbalbum_details[self._uuid]
|
||||
if details["customsortkey"] == 1:
|
||||
if details["customsortascending"] == 0:
|
||||
return AlbumSortOrder.NEWEST_FIRST
|
||||
elif details["customsortascending"] == 1:
|
||||
return AlbumSortOrder.OLDEST_FIRST
|
||||
else:
|
||||
return AlbumSortOrder.UNKNOWN
|
||||
elif details["customsortkey"] == 5:
|
||||
return AlbumSortOrder.TITLE
|
||||
elif details["customsortkey"] == 0:
|
||||
return AlbumSortOrder.MANUAL
|
||||
else:
|
||||
return AlbumSortOrder.UNKNOWN
|
||||
|
||||
def photo_index(self, photo):
|
||||
"""return index of photo in album (based on album sort order)"""
|
||||
for index, p in enumerate(self.photos):
|
||||
if p.uuid == photo.uuid:
|
||||
return index
|
||||
raise ValueError(
|
||||
f"Photo with uuid {photo.uuid} does not appear to be in this album"
|
||||
)
|
||||
|
||||
|
||||
class ImportInfo(AlbumInfoBaseClass):
|
||||
"""Information about import sessions"""
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in import session """
|
||||
"""return list of photos contained in import session"""
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
@@ -229,9 +294,18 @@ class ImportInfo(AlbumInfoBaseClass):
|
||||
return self._photos
|
||||
|
||||
|
||||
class ProjectInfo(AlbumInfo):
|
||||
"""
|
||||
ProjectInfo with info about projects
|
||||
Projects are cards, calendars, slideshows, etc.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class FolderInfo:
|
||||
"""
|
||||
Info about a specific folder, contains all the details about the folder
|
||||
Info about a specific folder, contains all the details about the folder
|
||||
including folders, albums, etc
|
||||
"""
|
||||
|
||||
@@ -247,17 +321,17 @@ class FolderInfo:
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" return title / name of folder"""
|
||||
"""return title / name of folder"""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" return uuid of folder """
|
||||
"""return uuid of folder"""
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" return list of albums (as AlbumInfo objects) contained in the folder """
|
||||
"""return list of albums (as AlbumInfo objects) contained in the folder"""
|
||||
try:
|
||||
return self._albums
|
||||
except AttributeError:
|
||||
@@ -282,7 +356,7 @@ class FolderInfo:
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
""" returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """
|
||||
"""returns FolderInfo object for parent or None if no parent (e.g. top-level folder)"""
|
||||
try:
|
||||
return self._parent
|
||||
except AttributeError:
|
||||
@@ -290,7 +364,7 @@ class FolderInfo:
|
||||
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||
else None
|
||||
)
|
||||
else:
|
||||
@@ -304,7 +378,7 @@ class FolderInfo:
|
||||
|
||||
@property
|
||||
def subfolders(self):
|
||||
""" return list of folders (as FolderInfo objects) contained in the folder """
|
||||
"""return list of folders (as FolderInfo objects) contained in the folder"""
|
||||
try:
|
||||
return self._folders
|
||||
except AttributeError:
|
||||
@@ -328,5 +402,5 @@ class FolderInfo:
|
||||
return self._folders
|
||||
|
||||
def __len__(self):
|
||||
""" returns count of folders + albums contained in the folder """
|
||||
"""returns count of folders + albums contained in the folder"""
|
||||
return len(self.subfolders) + len(self.album_info)
|
||||
|
||||
4282
osxphotos/cli.py
Normal file
4282
osxphotos/cli.py
Normal file
File diff suppressed because it is too large
Load Diff
348
osxphotos/cli_help.py
Normal file
348
osxphotos/cli_help.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""Help text helper class for osxphotos CLI """
|
||||
|
||||
import io
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import click
|
||||
import osxmetadata
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from ._constants import (
|
||||
EXTENDED_ATTRIBUTE_NAMES,
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||
OSXPHOTOS_EXPORT_DB,
|
||||
POST_COMMAND_CATEGORIES,
|
||||
)
|
||||
from .phototemplate import (
|
||||
TEMPLATE_SUBSTITUTIONS,
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||
TEMPLATE_SUBSTITUTIONS_PATHLIB,
|
||||
get_template_help,
|
||||
)
|
||||
|
||||
|
||||
# TODO: The following help text could probably be done as mako template
|
||||
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(rich_text("[bold]** Export **[/bold]", width=formatter.width))
|
||||
formatter.write("\n")
|
||||
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(
|
||||
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
|
||||
)
|
||||
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(
|
||||
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(template_help(width=formatter.width))
|
||||
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}" '
|
||||
+ "or in the 'folder>subfolder>album' format used in Lightroom Classic, --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(
|
||||
rich_text(
|
||||
"[bold]** Template Substitutions **[/bold]", width=formatter.width
|
||||
)
|
||||
)
|
||||
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)
|
||||
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"The following substitutions are file or directory paths. "
|
||||
+ "You can access various parts of the path using the following modifiers:"
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write("{path.parent}: the parent directory\n")
|
||||
formatter.write("{path.name}: the name of the file or final sub-directory\n")
|
||||
formatter.write("{path.stem}: the name of the file without the extension\n")
|
||||
formatter.write(
|
||||
"{path.suffix}: the suffix of the file including the leading '.'\n"
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
"For example, if the field {export_dir} is '/Shared/Backup/Photos':\n"
|
||||
)
|
||||
formatter.write("{export_dir.parent} is '/Shared/Backup'\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
"If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':\n"
|
||||
)
|
||||
formatter.write("{filepath.parent} is '/Shared/Backup/Photos'\n")
|
||||
formatter.write("{filepath.name} is 'IMG_1234.JPG'\n")
|
||||
formatter.write("{filepath.stem} is 'IMG_1234'\n")
|
||||
formatter.write("{filepath.suffix} is '.JPG'\n")
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS_PATHLIB.items())
|
||||
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
|
||||
)
|
||||
formatter.write_text(
|
||||
"You can run commands on the exported photos for post-processing "
|
||||
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
|
||||
+ "COMMAND is an osxphotos template string which will be rendered and passed to the shell "
|
||||
+ "for execution. CATEGORY is the category of file to pass to COMMAND. "
|
||||
+ "The following categories are available: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Category", "Description")]
|
||||
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"In addition to all normal template fields, the template fields "
|
||||
+ "'{filepath}' and '{export_dir}' will be available to your command template. "
|
||||
+ "Both of these are path-type templates which means their various parts can be accessed using "
|
||||
+ "the available properties, e.g. '{filepath.name}' provides just the file name without path "
|
||||
+ "and '{filepath.suffix}' is the file extension (suffix) of the file. "
|
||||
+ "When using paths in your command template, it is important to properly quote the paths "
|
||||
+ "as they will be passed to the shell and path names may contain spaces. "
|
||||
+ "Both the '{shell_quote}' template and the '|shell_quote' template filter are available for "
|
||||
+ "this purpose. For example, the following command outputs the full path of newly exported files to file 'new.txt': "
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
'--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text(
|
||||
"In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is properly quoted "
|
||||
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
|
||||
"If '{filepath}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
|
||||
"thus renders to: "
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write("echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'")
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text(
|
||||
"It is highly recommended that you run osxphotos with '--dry-run --verbose' "
|
||||
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
|
||||
+ "print out the exact command string which would be executed."
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
|
||||
)
|
||||
formatter.write_text(
|
||||
"You can run your own python functions on the exported photos for post-processing "
|
||||
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
|
||||
+ "and the name of the function in the file to call using format 'filename.py::function_name'. "
|
||||
+ "See the example function at https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py "
|
||||
+ "You may specify multiple functions to run by repeating the --post-function option. "
|
||||
+ "All post functions will be called immediately after export of each photo and immediately before any --post-command commands. "
|
||||
+ "Post functions will not be called if the --dry-run flag is set."
|
||||
)
|
||||
formatter.write("\n")
|
||||
|
||||
help_text += formatter.getvalue()
|
||||
return help_text
|
||||
|
||||
|
||||
def template_help(width=78):
|
||||
"""Return formatted string for template system"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
template_help_md = strip_md_header_and_links(get_template_help())
|
||||
console.print(Markdown(template_help_md))
|
||||
help_str = sio.getvalue()
|
||||
sio.close()
|
||||
return help_str
|
||||
|
||||
|
||||
def tutorial_help(width=78):
|
||||
"""Return formatted string for tutorial"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
help_md = get_tutorial_text()
|
||||
help_md = strip_html_comments(help_md)
|
||||
help_md = strip_md_links(help_md)
|
||||
console.print(Markdown(help_md))
|
||||
help_str = sio.getvalue()
|
||||
sio.close()
|
||||
return help_str
|
||||
|
||||
|
||||
def rich_text(text, width=78):
|
||||
"""Return rich formatted text"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
console.print(text)
|
||||
rich_text = sio.getvalue()
|
||||
sio.close()
|
||||
return rich_text
|
||||
|
||||
|
||||
def strip_md_header_and_links(md):
|
||||
"""strip markdown headers and links from markdown text md
|
||||
|
||||
Args:
|
||||
md: str, markdown text
|
||||
|
||||
Returns:
|
||||
str with markdown headers and links removed
|
||||
|
||||
Note: This uses a very basic regex that likely fails on all sorts of edge cases
|
||||
but works for the links in the osxphotos docs
|
||||
"""
|
||||
links = r"(?:[*#])|\[(.*?)\]\(.+?\)"
|
||||
|
||||
def subfn(match):
|
||||
return match.group(1)
|
||||
|
||||
return re.sub(links, subfn, md)
|
||||
|
||||
|
||||
def strip_md_links(md):
|
||||
"""strip markdown links from markdown text md
|
||||
|
||||
Args:
|
||||
md: str, markdown text
|
||||
|
||||
Returns:
|
||||
str with markdown links removed
|
||||
|
||||
Note: This uses a very basic regex that likely fails on all sorts of edge cases
|
||||
but works for the links in the osxphotos docs
|
||||
"""
|
||||
links = r"\[(.*?)\]\(.+?\)"
|
||||
|
||||
def subfn(match):
|
||||
return match.group(1)
|
||||
|
||||
return re.sub(links, subfn, md)
|
||||
|
||||
|
||||
def strip_html_comments(text):
|
||||
"""Strip html comments from text (which doesn't need to be valid HTML)"""
|
||||
return re.sub(r"<!--(.|\s|\n)*?-->", "", text)
|
||||
|
||||
|
||||
def get_tutorial_text():
|
||||
"""Load tutorial text from file"""
|
||||
# TODO: would be better to use importlib.abc.ResourceReader but I can't find a single example of how to do this
|
||||
help_file = pathlib.Path(__file__).parent / "tutorial.md"
|
||||
with open(help_file, "r") as fd:
|
||||
md = fd.read()
|
||||
return md
|
||||
@@ -6,22 +6,53 @@
|
||||
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
|
||||
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
|
||||
|
||||
import atexit
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
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)
|
||||
|
||||
# list of exiftool processes to cleanup when exiting or when terminate is called
|
||||
EXIFTOOL_PROCESSES = []
|
||||
|
||||
|
||||
def escape_str(s):
|
||||
"""escape string for use with exiftool -E"""
|
||||
if type(s) != str:
|
||||
return s
|
||||
s = html.escape(s)
|
||||
s = s.replace("\n", "
")
|
||||
s = s.replace("\t", "	")
|
||||
s = s.replace("\r", "
")
|
||||
return s
|
||||
|
||||
|
||||
def unescape_str(s):
|
||||
"""unescape an HTML string returned by exiftool -E"""
|
||||
if type(s) != str:
|
||||
return s
|
||||
return html.unescape(s)
|
||||
|
||||
|
||||
@atexit.register
|
||||
def terminate_exiftool():
|
||||
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""
|
||||
for proc in EXIFTOOL_PROCESSES:
|
||||
proc._stop_proc()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_exiftool_path():
|
||||
""" return path of exiftool, cache result """
|
||||
"""return path of exiftool, cache result"""
|
||||
exiftool_path = shutil.which("exiftool")
|
||||
if exiftool_path:
|
||||
return exiftool_path.rstrip()
|
||||
@@ -37,7 +68,7 @@ class _ExifToolProc:
|
||||
Creates a singleton object"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
""" create new object or return instance of already created singleton """
|
||||
"""create new object or return instance of already created singleton"""
|
||||
if not hasattr(cls, "instance") or not cls.instance:
|
||||
cls.instance = super().__new__(cls)
|
||||
|
||||
@@ -62,24 +93,25 @@ class _ExifToolProc:
|
||||
|
||||
@property
|
||||
def process(self):
|
||||
""" return the exiftool subprocess """
|
||||
"""return the exiftool subprocess"""
|
||||
if self._process_running:
|
||||
return self._process
|
||||
else:
|
||||
raise ValueError("exiftool process is not running")
|
||||
self._start_proc()
|
||||
return self._process
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
""" return process id (PID) of the exiftool process """
|
||||
"""return process id (PID) of the exiftool process"""
|
||||
return self._process.pid
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
""" return path to exiftool process """
|
||||
"""return path to exiftool process"""
|
||||
return self._exiftool
|
||||
|
||||
def _start_proc(self):
|
||||
""" start exiftool in batch mode """
|
||||
"""start exiftool in batch mode"""
|
||||
|
||||
if self._process_running:
|
||||
logging.warning("exiftool already running: {self._process}")
|
||||
@@ -97,6 +129,7 @@ class _ExifToolProc:
|
||||
"-n", # no print conversion (e.g. print tag values in machine readable format)
|
||||
"-P", # Preserve file modification date/time
|
||||
"-G", # print group name for each tag
|
||||
"-E", # escape tag values for HTML (allows use of HTML 
 for newlines)
|
||||
],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -104,33 +137,33 @@ class _ExifToolProc:
|
||||
)
|
||||
self._process_running = True
|
||||
|
||||
EXIFTOOL_PROCESSES.append(self)
|
||||
|
||||
def _stop_proc(self):
|
||||
""" stop the exiftool process if it's running, otherwise, do nothing """
|
||||
"""stop the exiftool process if it's running, otherwise, do nothing"""
|
||||
|
||||
if not self._process_running:
|
||||
return
|
||||
|
||||
self._process.stdin.write(b"-stay_open\n")
|
||||
self._process.stdin.write(b"False\n")
|
||||
self._process.stdin.flush()
|
||||
try:
|
||||
self._process.stdin.write(b"-stay_open\n")
|
||||
self._process.stdin.write(b"False\n")
|
||||
self._process.stdin.flush()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.warning(
|
||||
f"exiftool pid {self._process.pid} did not exit, killing it"
|
||||
)
|
||||
self._process.kill()
|
||||
self._process.communicate()
|
||||
|
||||
del self._process
|
||||
self._process_running = False
|
||||
|
||||
def __del__(self):
|
||||
self._stop_proc()
|
||||
|
||||
|
||||
class ExifTool:
|
||||
""" Basic exiftool interface for reading and writing EXIF tags """
|
||||
"""Basic exiftool interface for reading and writing EXIF tags"""
|
||||
|
||||
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
|
||||
"""Create ExifTool object
|
||||
@@ -153,9 +186,12 @@ class ExifTool:
|
||||
# if running as a context manager, self._context_mgr will be True
|
||||
self._context_mgr = False
|
||||
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
|
||||
self._process = self._exiftoolproc.process
|
||||
self._read_exif()
|
||||
|
||||
@property
|
||||
def _process(self):
|
||||
return self._exiftoolproc.process
|
||||
|
||||
def setvalue(self, tag, value):
|
||||
"""Set tag to value(s); if value is None, will delete tag
|
||||
|
||||
@@ -173,15 +209,20 @@ class ExifTool:
|
||||
|
||||
if value is None:
|
||||
value = ""
|
||||
value = escape_str(value)
|
||||
command = [f"-{tag}={value}"]
|
||||
if self.overwrite and not self._context_mgr:
|
||||
command.append("-overwrite_original")
|
||||
|
||||
# avoid "Warning: Some character(s) could not be encoded in Latin" warning
|
||||
command.append("-iptc:codedcharacterset=utf8")
|
||||
|
||||
if self._context_mgr:
|
||||
self._commands.extend(command)
|
||||
return True
|
||||
else:
|
||||
_, _, error = self.run_commands(*command)
|
||||
return error is None
|
||||
return error == ""
|
||||
|
||||
def addvalues(self, tag, *values):
|
||||
"""Add one or more value(s) to tag
|
||||
@@ -213,6 +254,7 @@ class ExifTool:
|
||||
for value in values:
|
||||
if value is None:
|
||||
raise ValueError("Can't add None value to tag")
|
||||
value = escape_str(value)
|
||||
command.append(f"-{tag}+={value}")
|
||||
|
||||
if self.overwrite and not self._context_mgr:
|
||||
@@ -223,7 +265,7 @@ class ExifTool:
|
||||
return True
|
||||
else:
|
||||
_, _, error = self.run_commands(*command)
|
||||
return error is None
|
||||
return error == ""
|
||||
|
||||
def run_commands(self, *commands, no_file=False):
|
||||
"""Run commands in the exiftool process and return result.
|
||||
@@ -254,7 +296,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""
|
||||
@@ -291,27 +337,34 @@ class ExifTool:
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
""" return process id (PID) of the exiftool process """
|
||||
"""return process id (PID) of the exiftool process"""
|
||||
return self._process.pid
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
""" returns exiftool version """
|
||||
"""returns exiftool version"""
|
||||
ver, _, _ = self.run_commands("-ver", no_file=True)
|
||||
return ver.decode("utf-8")
|
||||
|
||||
def asdict(self, tag_groups=True):
|
||||
def asdict(self, tag_groups=True, normalized=False):
|
||||
"""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"
|
||||
normalized: if True, dict keys are all normalized to lower case (default is False)
|
||||
"""
|
||||
json_str, _, _ = self.run_commands("-json")
|
||||
if not json_str:
|
||||
return dict()
|
||||
json_str = unescape_str(json_str.decode("utf-8"))
|
||||
|
||||
exifdict = json.loads(json_str)
|
||||
try:
|
||||
exifdict = json.loads(json_str)
|
||||
except Exception as e:
|
||||
# will fail with some commands, e.g --ext AVI which produces
|
||||
# 'No file with specified extension' instead of json
|
||||
return dict()
|
||||
exifdict = exifdict[0]
|
||||
if not tag_groups:
|
||||
# strip tag groups
|
||||
@@ -320,15 +373,20 @@ class ExifTool:
|
||||
k = re.sub(r".*:", "", k)
|
||||
exif_new[k] = v
|
||||
exifdict = exif_new
|
||||
|
||||
if normalized:
|
||||
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||
|
||||
return exifdict
|
||||
|
||||
def json(self):
|
||||
""" returns JSON string containing all EXIF tags and values from exiftool """
|
||||
"""returns JSON string containing all EXIF tags and values from exiftool"""
|
||||
json, _, _ = self.run_commands("-json")
|
||||
json = unescape_str(json.decode("utf-8"))
|
||||
return json
|
||||
|
||||
def _read_exif(self):
|
||||
""" read exif data from file """
|
||||
"""read exif data from file"""
|
||||
data = self.asdict()
|
||||
self.data = {k: v for k, v in data.items()}
|
||||
|
||||
@@ -346,3 +404,73 @@ class ExifTool:
|
||||
elif self._commands:
|
||||
# run_commands sets self.warning and self.error as needed
|
||||
self.run_commands(*self._commands)
|
||||
|
||||
|
||||
class ExifToolCaching(ExifTool):
|
||||
"""Basic exiftool interface for reading and writing EXIF tags, with caching.
|
||||
Use this only when you know the file's EXIF data will not be changed by any external process.
|
||||
|
||||
Creates a singleton cached ExifTool instance"""
|
||||
|
||||
_singletons = {}
|
||||
|
||||
def __new__(cls, filepath, exiftool=None):
|
||||
"""create new object or return instance of already created singleton"""
|
||||
if filepath not in cls._singletons:
|
||||
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
|
||||
return cls._singletons[filepath]
|
||||
|
||||
|
||||
class _ExifToolCaching(ExifTool):
|
||||
def __init__(self, filepath, exiftool=None):
|
||||
"""Create read-only ExifTool object that caches values
|
||||
|
||||
Args:
|
||||
file: path to image file
|
||||
exiftool: path to exiftool, if not specified will look in path
|
||||
|
||||
Returns:
|
||||
ExifTool instance
|
||||
"""
|
||||
self._json_cache = None
|
||||
self._asdict_cache = {}
|
||||
super().__init__(filepath, exiftool=exiftool, overwrite=False, flags=None)
|
||||
|
||||
def run_commands(self, *commands, no_file=False):
|
||||
if commands[0] not in ["-json", "-ver"]:
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
return super().run_commands(*commands, no_file=no_file)
|
||||
|
||||
def setvalue(self, tag, value):
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
|
||||
def addvalues(self, tag, *values):
|
||||
raise NotImplementedError(f"{self.__class__} is read-only")
|
||||
|
||||
def json(self):
|
||||
if not self._json_cache:
|
||||
self._json_cache = super().json()
|
||||
return self._json_cache
|
||||
|
||||
def asdict(self, tag_groups=True, normalized=False):
|
||||
"""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"
|
||||
normalized: if True, dict keys are all normalized to lower case (default is False)
|
||||
"""
|
||||
try:
|
||||
return self._asdict_cache[tag_groups][normalized]
|
||||
except KeyError:
|
||||
if tag_groups not in self._asdict_cache:
|
||||
self._asdict_cache[tag_groups] = {}
|
||||
self._asdict_cache[tag_groups][normalized] = super().asdict(
|
||||
tag_groups=tag_groups, normalized=normalized
|
||||
)
|
||||
return self._asdict_cache[tag_groups][normalized]
|
||||
|
||||
def flush_cache(self):
|
||||
"""Clear cached data so that calls to json or asdict return fresh data"""
|
||||
self._json_cache = None
|
||||
self._asdict_cache = {}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
""" Helper class for managing a database used by
|
||||
PhotoInfo.export for tracking state of exports and updates
|
||||
"""
|
||||
""" Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
@@ -12,13 +10,15 @@ from abc import ABC, abstractmethod
|
||||
from io import StringIO
|
||||
from sqlite3 import Error
|
||||
|
||||
from ._constants import OSXPHOTOS_EXPORT_DB
|
||||
from ._version import __version__
|
||||
|
||||
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
|
||||
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
|
||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
|
||||
|
||||
|
||||
class ExportDB_ABC(ABC):
|
||||
""" abstract base class for ExportDB """
|
||||
"""abstract base class for ExportDB"""
|
||||
|
||||
@abstractmethod
|
||||
def get_uuid_for_file(self, filename):
|
||||
@@ -84,6 +84,18 @@ class ExportDB_ABC(ABC):
|
||||
def get_sidecar_for_file(self, filename):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_previous_uuids(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_detected_text_for_uuid(self, uuid):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_detected_text_for_uuid(self, uuid, json_text):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_data(
|
||||
self,
|
||||
@@ -100,7 +112,7 @@ class ExportDB_ABC(ABC):
|
||||
|
||||
|
||||
class ExportDBNoOp(ExportDB_ABC):
|
||||
""" An ExportDB with NoOp methods """
|
||||
"""An ExportDB with NoOp methods"""
|
||||
|
||||
def __init__(self):
|
||||
self.was_created = True
|
||||
@@ -155,6 +167,15 @@ class ExportDBNoOp(ExportDB_ABC):
|
||||
def get_sidecar_for_file(self, filename):
|
||||
return None, (None, None, None)
|
||||
|
||||
def get_previous_uuids(self):
|
||||
return []
|
||||
|
||||
def get_detected_text_for_uuid(self, uuid):
|
||||
return None
|
||||
|
||||
def set_detected_text_for_uuid(self, uuid, json_text):
|
||||
pass
|
||||
|
||||
def set_data(
|
||||
self,
|
||||
filename,
|
||||
@@ -170,23 +191,23 @@ class ExportDBNoOp(ExportDB_ABC):
|
||||
|
||||
|
||||
class ExportDB(ExportDB_ABC):
|
||||
""" Interface to sqlite3 database used to store state information for osxphotos export command """
|
||||
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
|
||||
|
||||
def __init__(self, dbfile):
|
||||
""" dbfile: path to osxphotos export database file """
|
||||
"""dbfile: path to osxphotos export database file"""
|
||||
self._dbfile = dbfile
|
||||
# _path is parent of the database
|
||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this parent _path
|
||||
# this allows the entire export tree to be moved to a new disk/location
|
||||
# whilst preserving the UUID to filename mappping
|
||||
# whilst preserving the UUID to filename mapping
|
||||
self._path = pathlib.Path(dbfile).parent
|
||||
self._conn = self._open_export_db(dbfile)
|
||||
self._insert_run_info()
|
||||
|
||||
def get_uuid_for_file(self, filename):
|
||||
""" query database for filename and return UUID
|
||||
returns None if filename not found in database
|
||||
"""query database for filename and return UUID
|
||||
returns None if filename not found in database
|
||||
"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
@@ -204,7 +225,7 @@ class ExportDB(ExportDB_ABC):
|
||||
return uuid
|
||||
|
||||
def set_uuid_for_file(self, filename, uuid):
|
||||
""" set UUID of filename to uuid in the database """
|
||||
"""set UUID of filename to uuid in the database"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||
filename_normalized = filename.lower()
|
||||
conn = self._conn
|
||||
@@ -219,9 +240,9 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def set_stat_orig_for_file(self, filename, stats):
|
||||
""" set stat info for filename
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime """
|
||||
"""set stat info for filename
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
if len(stats) != 3:
|
||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||
@@ -240,8 +261,8 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def get_stat_orig_for_file(self, filename):
|
||||
""" get stat info for filename
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""get stat info for filename
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
@@ -265,21 +286,21 @@ class ExportDB(ExportDB_ABC):
|
||||
return stats
|
||||
|
||||
def set_stat_edited_for_file(self, filename, stats):
|
||||
""" set stat info for edited version of image (in Photos' library)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime """
|
||||
"""set stat info for edited version of image (in Photos' library)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime"""
|
||||
return self._set_stat_for_file("edited", filename, stats)
|
||||
|
||||
def get_stat_edited_for_file(self, filename):
|
||||
""" get stat info for edited version of image (in Photos' library)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime """
|
||||
"""get stat info for edited version of image (in Photos' library)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime"""
|
||||
return self._get_stat_for_file("edited", filename)
|
||||
|
||||
def set_stat_exif_for_file(self, filename, stats):
|
||||
""" set stat info for filename (after exiftool has updated it)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime """
|
||||
"""set stat info for filename (after exiftool has updated it)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
if len(stats) != 3:
|
||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||
@@ -298,8 +319,8 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def get_stat_exif_for_file(self, filename):
|
||||
""" get stat info for filename (after exiftool has updated it)
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""get stat info for filename (after exiftool has updated it)
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
@@ -323,19 +344,19 @@ class ExportDB(ExportDB_ABC):
|
||||
return stats
|
||||
|
||||
def set_stat_converted_for_file(self, filename, stats):
|
||||
""" set stat info for filename (after image converted to jpeg)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime """
|
||||
"""set stat info for filename (after image converted to jpeg)
|
||||
filename: filename to set the stat info for
|
||||
stat: a tuple of length 3: mode, size, mtime"""
|
||||
return self._set_stat_for_file("converted", filename, stats)
|
||||
|
||||
def get_stat_converted_for_file(self, filename):
|
||||
""" get stat info for filename (after jpeg conversion)
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""get stat info for filename (after jpeg conversion)
|
||||
returns: tuple of (mode, size, mtime)
|
||||
"""
|
||||
return self._get_stat_for_file("converted", filename)
|
||||
|
||||
def get_info_for_uuid(self, uuid):
|
||||
""" returns the info JSON struct for a UUID """
|
||||
"""returns the info JSON struct for a UUID"""
|
||||
conn = self._conn
|
||||
try:
|
||||
c = conn.cursor()
|
||||
@@ -349,7 +370,7 @@ class ExportDB(ExportDB_ABC):
|
||||
return info
|
||||
|
||||
def set_info_for_uuid(self, uuid, info):
|
||||
""" sets the info JSON struct for a UUID """
|
||||
"""sets the info JSON struct for a UUID"""
|
||||
conn = self._conn
|
||||
try:
|
||||
c = conn.cursor()
|
||||
@@ -362,7 +383,7 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def get_exifdata_for_file(self, filename):
|
||||
""" returns the exifdata JSON struct for a file """
|
||||
"""returns the exifdata JSON struct for a file"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
try:
|
||||
@@ -380,7 +401,7 @@ class ExportDB(ExportDB_ABC):
|
||||
return exifdata
|
||||
|
||||
def set_exifdata_for_file(self, filename, exifdata):
|
||||
""" sets the exifdata JSON struct for a file """
|
||||
"""sets the exifdata JSON struct for a file"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
try:
|
||||
@@ -394,7 +415,7 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def get_sidecar_for_file(self, filename):
|
||||
""" returns the sidecar data and signature for a file """
|
||||
"""returns the sidecar data and signature for a file"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
try:
|
||||
@@ -422,7 +443,7 @@ class ExportDB(ExportDB_ABC):
|
||||
return sidecar_data, sidecar_sig
|
||||
|
||||
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
|
||||
""" sets the sidecar data and signature for a file """
|
||||
"""sets the sidecar data and signature for a file"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||
conn = self._conn
|
||||
try:
|
||||
@@ -435,6 +456,49 @@ 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 get_detected_text_for_uuid(self, uuid):
|
||||
"""Get the detected_text for a uuid"""
|
||||
conn = self._conn
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
"SELECT text_data FROM detected_text WHERE uuid = ?",
|
||||
(uuid,),
|
||||
)
|
||||
results = c.fetchone()
|
||||
detected_text = results[0] if results else None
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
detected_text = None
|
||||
|
||||
return detected_text
|
||||
|
||||
def set_detected_text_for_uuid(self, uuid, text_json):
|
||||
"""Set the detected text for uuid"""
|
||||
conn = self._conn
|
||||
try:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
|
||||
(uuid, text_json,),
|
||||
)
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
|
||||
def set_data(
|
||||
self,
|
||||
filename,
|
||||
@@ -446,8 +510,7 @@ class ExportDB(ExportDB_ABC):
|
||||
info_json,
|
||||
exif_json,
|
||||
):
|
||||
""" sets all the data for file and uuid at once
|
||||
"""
|
||||
"""sets all the data for file and uuid at once"""
|
||||
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||
filename_normalized = filename.lower()
|
||||
conn = self._conn
|
||||
@@ -490,7 +553,7 @@ class ExportDB(ExportDB_ABC):
|
||||
logging.warning(e)
|
||||
|
||||
def close(self):
|
||||
""" close the database connection """
|
||||
"""close the database connection"""
|
||||
try:
|
||||
self._conn.close()
|
||||
except Error as e:
|
||||
@@ -528,9 +591,9 @@ class ExportDB(ExportDB_ABC):
|
||||
return stats
|
||||
|
||||
def _open_export_db(self, dbfile):
|
||||
""" open export database and return a db connection
|
||||
if dbfile does not exist, will create and initialize the database
|
||||
returns: connection to the database
|
||||
"""open export database and return a db connection
|
||||
if dbfile does not exist, will create and initialize the database
|
||||
returns: connection to the database
|
||||
"""
|
||||
|
||||
if not os.path.isfile(dbfile):
|
||||
@@ -553,7 +616,7 @@ class ExportDB(ExportDB_ABC):
|
||||
return conn
|
||||
|
||||
def _get_db_connection(self, dbfile):
|
||||
""" return db connection to dbname """
|
||||
"""return db connection to dbname"""
|
||||
try:
|
||||
conn = sqlite3.connect(dbfile)
|
||||
except Error as e:
|
||||
@@ -563,15 +626,15 @@ class ExportDB(ExportDB_ABC):
|
||||
return conn
|
||||
|
||||
def _get_database_version(self, conn):
|
||||
""" return tuple of (osxphotos, exportdb) versions for database connection conn """
|
||||
"""return tuple of (osxphotos, exportdb) versions for database connection conn"""
|
||||
version_info = conn.execute(
|
||||
"SELECT osxphotos, exportdb, max(id) FROM version"
|
||||
).fetchone()
|
||||
return (version_info[0], version_info[1])
|
||||
|
||||
def _create_db_tables(self, conn):
|
||||
""" create (if not already created) the necessary db tables for the export database
|
||||
conn: sqlite3 db connection
|
||||
"""create (if not already created) the necessary db tables for the export database
|
||||
conn: sqlite3 db connection
|
||||
"""
|
||||
sql_commands = {
|
||||
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
|
||||
@@ -579,6 +642,10 @@ class ExportDB(ExportDB_ABC):
|
||||
osxphotos TEXT,
|
||||
exportdb TEXT
|
||||
); """,
|
||||
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
|
||||
id INTEGER PRIMARY KEY,
|
||||
about TEXT
|
||||
);""",
|
||||
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
filepath TEXT NOT NULL,
|
||||
@@ -631,12 +698,18 @@ class ExportDB(ExportDB_ABC):
|
||||
size INTEGER,
|
||||
mtime REAL
|
||||
); """,
|
||||
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
|
||||
id INTEGER PRIMARY KEY,
|
||||
uuid TEXT NOT NULL,
|
||||
text_data JSON
|
||||
); """,
|
||||
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
|
||||
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
|
||||
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
|
||||
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
|
||||
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
|
||||
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
|
||||
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
|
||||
}
|
||||
try:
|
||||
c = conn.cursor()
|
||||
@@ -646,12 +719,13 @@ class ExportDB(ExportDB_ABC):
|
||||
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
|
||||
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
|
||||
)
|
||||
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
logging.warning(e)
|
||||
|
||||
def __del__(self):
|
||||
""" ensure the database connection is closed """
|
||||
"""ensure the database connection is closed"""
|
||||
try:
|
||||
self._conn.close()
|
||||
except:
|
||||
@@ -676,35 +750,33 @@ class ExportDB(ExportDB_ABC):
|
||||
|
||||
|
||||
class ExportDBInMemory(ExportDB):
|
||||
""" In memory version of ExportDB
|
||||
Copies the on-disk database into memory so it may be operated on without
|
||||
modifying the on-disk verison
|
||||
"""In memory version of ExportDB
|
||||
Copies the on-disk database into memory so it may be operated on without
|
||||
modifying the on-disk version
|
||||
"""
|
||||
|
||||
def init(self, dbfile):
|
||||
self._dbfile = dbfile
|
||||
def __init__(self, dbfile):
|
||||
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
|
||||
# _path is parent of the database
|
||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
||||
# relative paths to this parent _path
|
||||
# this allows the entire export tree to be moved to a new disk/location
|
||||
# whilst preserving the UUID to filename mappping
|
||||
self._path = pathlib.Path(dbfile).parent
|
||||
self._conn = self._open_export_db(dbfile)
|
||||
# whilst preserving the UUID to filename mapping
|
||||
self._path = pathlib.Path(self._dbfile).parent
|
||||
self._conn = self._open_export_db(self._dbfile)
|
||||
self._insert_run_info()
|
||||
|
||||
def _open_export_db(self, dbfile):
|
||||
""" open export database and return a db connection
|
||||
returns: connection to the database
|
||||
"""open export database and return a db connection
|
||||
returns: connection to the database
|
||||
"""
|
||||
if not os.path.isfile(dbfile):
|
||||
conn = self._get_db_connection()
|
||||
if conn:
|
||||
self._create_db_tables(conn)
|
||||
self.was_created = True
|
||||
self.was_upgraded = ()
|
||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
||||
else:
|
||||
if not conn:
|
||||
raise Exception("Error getting connection to in-memory database")
|
||||
self._create_db_tables(conn)
|
||||
self.was_created = True
|
||||
self.was_upgraded = ()
|
||||
else:
|
||||
try:
|
||||
conn = sqlite3.connect(dbfile)
|
||||
@@ -729,12 +801,11 @@ class ExportDBInMemory(ExportDB):
|
||||
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
|
||||
else:
|
||||
self.was_upgraded = ()
|
||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
||||
|
||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
||||
return conn
|
||||
|
||||
def _get_db_connection(self):
|
||||
""" return db connection to in memory database """
|
||||
"""return db connection to in memory database"""
|
||||
try:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
except Error as e:
|
||||
|
||||
@@ -7,7 +7,7 @@ import subprocess
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
|
||||
from .imageconverter import ImageConverter
|
||||
|
||||
@@ -114,7 +114,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
if dest.is_dir():
|
||||
dest /= src.name
|
||||
|
||||
filemgr = CoreFoundation.NSFileManager.defaultManager()
|
||||
filemgr = Foundation.NSFileManager.defaultManager()
|
||||
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
|
||||
# error is a tuple of (bool, error_string)
|
||||
# error[0] is True if copy succeeded
|
||||
|
||||
@@ -121,6 +121,6 @@ class ImageConverter:
|
||||
return True
|
||||
else:
|
||||
raise ImageConversionError(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
)
|
||||
|
||||
|
||||
69
osxphotos/momentinfo.py
Normal file
69
osxphotos/momentinfo.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""MomentInfo class with details about photo moments."""
|
||||
|
||||
|
||||
class MomentInfo:
|
||||
"""Info about a photo moment"""
|
||||
|
||||
def __init__(self, db, moment_pk):
|
||||
"""Initialize with a moment PK; returns None if PK not found."""
|
||||
self._db = db
|
||||
self._pk = moment_pk
|
||||
|
||||
self._moment = self._db._db_moment_pk.get(moment_pk)
|
||||
if not self._moment:
|
||||
raise ValueError(f"No moment with PK {moment_pk}")
|
||||
|
||||
@property
|
||||
def pk(self):
|
||||
"""Primary key of the moment."""
|
||||
return self._pk
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
"""Location of the moment."""
|
||||
return (self._moment.get("latitude"), self._moment.get("longitude"))
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Title of the moment."""
|
||||
return self._moment.get("title")
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
"""Subtitle of the moment."""
|
||||
return self._moment.get("subtitle")
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
"""Start date of the moment."""
|
||||
return self._moment.get("startDate")
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
"""Stop date of the moment."""
|
||||
return self._moment.get("endDate")
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""Date of the moment."""
|
||||
return self._moment.get("representativeDate")
|
||||
|
||||
@property
|
||||
def modification_date(self):
|
||||
"""Modification date of the moment."""
|
||||
return self._moment.get("modificationDate")
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
"""All photos in this moment"""
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
photo_uuids = [
|
||||
uuid
|
||||
for uuid, photo in self._db._dbphotos.items()
|
||||
if photo["momentID"] == self._pk
|
||||
]
|
||||
|
||||
self._photos = self._db.photos_by_uuid(photo_uuids)
|
||||
return self._photos
|
||||
@@ -6,22 +6,22 @@ from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||
|
||||
|
||||
def sanitize_filepath(filepath):
|
||||
""" sanitize a filepath """
|
||||
"""sanitize a filepath"""
|
||||
return pathvalidate.sanitize_filepath(filepath, platform="macos")
|
||||
|
||||
|
||||
def is_valid_filepath(filepath):
|
||||
""" returns True if a filepath is valid otherwise False """
|
||||
"""returns True if a filepath is valid otherwise False"""
|
||||
return pathvalidate.is_valid_filepath(filepath, platform="macos")
|
||||
|
||||
|
||||
def sanitize_filename(filename, replacement=":"):
|
||||
""" replace any illegal characters in a filename and truncate filename if needed
|
||||
"""replace any illegal characters in a filename and truncate filename if needed
|
||||
|
||||
Args:
|
||||
filename: str, filename to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
|
||||
Returns:
|
||||
filename with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
@@ -46,12 +46,12 @@ def sanitize_filename(filename, replacement=":"):
|
||||
|
||||
|
||||
def sanitize_dirname(dirname, replacement=":"):
|
||||
""" replace any illegal characters in a directory name and truncate directory name if needed
|
||||
"""replace any illegal characters in a directory name and truncate directory name if needed
|
||||
|
||||
Args:
|
||||
dirname: str, directory name to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
dirname: str, directory name to sanitize
|
||||
replacement: str, value to replace any illegal characters with; default = ":"; if None, no replacement occurs
|
||||
|
||||
Returns:
|
||||
dirname with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
@@ -61,19 +61,20 @@ def sanitize_dirname(dirname, replacement=":"):
|
||||
|
||||
|
||||
def sanitize_pathpart(pathpart, replacement=":"):
|
||||
""" replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
|
||||
"""replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
|
||||
|
||||
Args:
|
||||
pathpart: str, path part to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
pathpart: str, path part to sanitize
|
||||
replacement: str, value to replace any illegal characters with; default = ":"; if None, no replacement occurs
|
||||
|
||||
Returns:
|
||||
pathpart with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
if pathpart:
|
||||
pathpart = pathpart.replace("/", replacement)
|
||||
pathpart = (
|
||||
pathpart.replace("/", replacement) if replacement is not None else pathpart
|
||||
)
|
||||
if len(pathpart) > MAX_DIRNAME_LEN:
|
||||
drop = len(pathpart) - MAX_DIRNAME_LEN
|
||||
pathpart = pathpart[:-drop]
|
||||
return pathpart
|
||||
|
||||
|
||||
@@ -11,16 +11,15 @@ MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
||||
|
||||
|
||||
class PersonInfo:
|
||||
""" Info about a person in the Photos library
|
||||
"""
|
||||
"""Info about a person in the Photos library"""
|
||||
|
||||
def __init__(self, db=None, pk=None):
|
||||
""" Creates a new PersonInfo instance
|
||||
"""Creates a new PersonInfo instance
|
||||
|
||||
Arguments:
|
||||
db: instance of PhotosDB object
|
||||
pk: primary key value of person to initialize PersonInfo with
|
||||
|
||||
pk: primary key value of person to initialize PersonInfo with
|
||||
|
||||
Returns:
|
||||
PersonInfo instance
|
||||
"""
|
||||
@@ -57,8 +56,8 @@ class PersonInfo:
|
||||
|
||||
@property
|
||||
def face_info(self):
|
||||
""" Returns a list of FaceInfo objects associated with this person sorted by quality score
|
||||
Highest quality face is result[0] and lowest quality face is result[n]
|
||||
"""Returns a list of FaceInfo objects associated with this person sorted by quality score
|
||||
Highest quality face is result[0] and lowest quality face is result[n]
|
||||
"""
|
||||
try:
|
||||
faces = self._db._db_faceinfo_person[self._pk]
|
||||
@@ -103,16 +102,15 @@ class PersonInfo:
|
||||
|
||||
|
||||
class FaceInfo:
|
||||
""" Info about a face in the Photos library
|
||||
"""
|
||||
"""Info about a face in the Photos library"""
|
||||
|
||||
def __init__(self, db=None, pk=None):
|
||||
""" Creates a new FaceInfo instance
|
||||
"""Creates a new FaceInfo instance
|
||||
|
||||
Arguments:
|
||||
db: instance of PhotosDB object
|
||||
pk: primary key value of face to init the object with
|
||||
|
||||
pk: primary key value of face to init the object with
|
||||
|
||||
Returns:
|
||||
FaceInfo instance
|
||||
"""
|
||||
@@ -143,7 +141,7 @@ class FaceInfo:
|
||||
self.manual = face["manual"]
|
||||
self.face_type = face["facetype"]
|
||||
self.age_type = face["agetype"]
|
||||
self.bald_type = face["baldtype"]
|
||||
# self.bald_type = face["baldtype"]
|
||||
self.eye_makeup_type = face["eyemakeuptype"]
|
||||
self.eye_state = face["eyestate"]
|
||||
self.facial_hair_type = face["facialhairtype"]
|
||||
@@ -156,7 +154,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
""" Coordinates, in PIL format, for center of face
|
||||
"""Coordinates, in PIL format, for center of face
|
||||
|
||||
Returns:
|
||||
tuple of coordinates in form (x, y)
|
||||
@@ -165,7 +163,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def size_pixels(self):
|
||||
""" Size of face in pixels (centered around center_x, center_y)
|
||||
"""Size of face in pixels (centered around center_x, center_y)
|
||||
|
||||
Returns:
|
||||
size, in int pixels, of a circle drawn around the center of the face
|
||||
@@ -176,7 +174,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def mouth(self):
|
||||
""" Coordinates, in PIL format, for mouth position
|
||||
"""Coordinates, in PIL format, for mouth position
|
||||
|
||||
Returns:
|
||||
tuple of coordinates in form (x, y)
|
||||
@@ -185,7 +183,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def left_eye(self):
|
||||
""" Coordinates, in PIL format, for left eye position
|
||||
"""Coordinates, in PIL format, for left eye position
|
||||
|
||||
Returns:
|
||||
tuple of coordinates in form (x, y)
|
||||
@@ -194,7 +192,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def right_eye(self):
|
||||
""" Coordinates, in PIL format, for right eye position
|
||||
"""Coordinates, in PIL format, for right eye position
|
||||
|
||||
Returns:
|
||||
tuple of coordinates in form (x, y)
|
||||
@@ -223,7 +221,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def mwg_rs_area(self):
|
||||
""" Get coordinates for Metadata Working Group Region Area.
|
||||
"""Get coordinates for Metadata Working Group Region Area.
|
||||
|
||||
Returns:
|
||||
MWG_RS_Area named tuple with x, y, h, w where:
|
||||
@@ -249,7 +247,7 @@ class FaceInfo:
|
||||
|
||||
@property
|
||||
def mpri_reg_rect(self):
|
||||
""" Get coordinates for Microsoft Photo Region Rectangle.
|
||||
"""Get coordinates for Microsoft Photo Region Rectangle.
|
||||
|
||||
Returns:
|
||||
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
||||
@@ -278,7 +276,7 @@ class FaceInfo:
|
||||
return MPRI_Reg_Rect(x, y, h, w)
|
||||
|
||||
def face_rect(self):
|
||||
""" Get face rectangle coordinates for current version of the associated image
|
||||
"""Get face rectangle coordinates for current version of the associated image
|
||||
If image has been edited, rectangle applies to edited version, otherwise original version
|
||||
Coordinates in format and reference frame used by PIL
|
||||
|
||||
@@ -321,12 +319,12 @@ class FaceInfo:
|
||||
return yaw
|
||||
|
||||
def _fix_orientation(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
"""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
|
||||
"""
|
||||
@@ -350,21 +348,24 @@ class FaceInfo:
|
||||
elif orientation == 7:
|
||||
x, y = y, x
|
||||
y = 1.0 - y
|
||||
elif orientation ==8:
|
||||
elif orientation == 8:
|
||||
x, y = y, x
|
||||
elif orientation == 0:
|
||||
# set by osxphotos if adjusted orientation cannot be read, assume it's 1
|
||||
y = 1.0 - y
|
||||
else:
|
||||
logging.warning(f"Unhandled orientation: {orientation}")
|
||||
|
||||
return (x, y)
|
||||
|
||||
def _make_point(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
"""Translate an (x, y) tuple based on image orientation
|
||||
and convert to image coordinates
|
||||
|
||||
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 in pixels in PIL format/reference frame
|
||||
"""
|
||||
@@ -379,13 +380,13 @@ class FaceInfo:
|
||||
return (int(x * dx), int(y * dy))
|
||||
|
||||
def _make_point_with_rotation(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation and rotation
|
||||
"""Translate an (x, y) tuple based on image orientation and rotation
|
||||
and convert to image coordinates
|
||||
|
||||
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 in pixels in PIL format/reference frame
|
||||
"""
|
||||
@@ -437,7 +438,7 @@ class FaceInfo:
|
||||
"manual": self.manual,
|
||||
"face_type": self.face_type,
|
||||
"age_type": self.age_type,
|
||||
"bald_type": self.bald_type,
|
||||
# "bald_type": self.bald_type,
|
||||
"eye_makeup_type": self.eye_makeup_type,
|
||||
"eye_state": self.eye_state,
|
||||
"facial_hair_type": self.facial_hair_type,
|
||||
@@ -472,14 +473,14 @@ class FaceInfo:
|
||||
|
||||
|
||||
def rotate_image_point(x, y, xmid, ymid, angle):
|
||||
""" rotate image point about xm, ym by angle in radians
|
||||
"""rotate image point about xm, ym by angle in radians
|
||||
|
||||
Arguments:
|
||||
x: x coordinate of point to rotate
|
||||
x: x coordinate of point to rotate
|
||||
y: y coordinate of point to rotate
|
||||
xmid: x coordinate of center point to rotate about
|
||||
ymid: y coordinate of center point to rotate about
|
||||
angle: angle in radians about which to coordinate,
|
||||
angle: angle in radians about which to coordinate,
|
||||
counter-clockwise is positive
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -7,4 +7,4 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
from ._photoinfo_exifinfo import ExifInfo
|
||||
from ._photoinfo_export import ExportResults
|
||||
from ._photoinfo_scoreinfo import ScoreInfo
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photoinfo import PhotoInfo, PhotoInfoNone
|
||||
@@ -3,13 +3,13 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..exiftool import ExifTool, get_exiftool_path
|
||||
from ..exiftool import ExifToolCaching, get_exiftool_path
|
||||
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
""" Returns an ExifTool object for the photo
|
||||
requires that exiftool (https://exiftool.org/) be installed
|
||||
""" Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
||||
Requires that exiftool (https://exiftool.org/) be installed
|
||||
If exiftool not installed, logs warning and returns None
|
||||
If photo path is missing, returns None
|
||||
"""
|
||||
@@ -20,7 +20,7 @@ def exiftool(self):
|
||||
try:
|
||||
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_path)
|
||||
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
||||
else:
|
||||
exiftool = None
|
||||
except FileNotFoundError:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,6 @@
|
||||
"""
|
||||
|
||||
# NOTES:
|
||||
# - This likely leaks memory like a sieve as I need to ensure all the
|
||||
# Objective C objects are cleaned up.
|
||||
# - There are several techniques used for handling PhotoKit's various
|
||||
# asynchronous calls used in this code: event loop+notification, threading
|
||||
# event, while loop. I've experimented with each to find the one that works.
|
||||
@@ -34,7 +32,8 @@ from Foundation import NSNotificationCenter, NSObject
|
||||
from PyObjCTools import AppHelper
|
||||
|
||||
from .fileutil import FileUtil
|
||||
from .utils import _get_os_version, get_preferred_uti_extension, increment_filename
|
||||
from .uti import get_preferred_uti_extension
|
||||
from .utils import _get_os_version, increment_filename
|
||||
|
||||
# NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm)
|
||||
# to access Photos. This should happen automatically the first time it's called. I've
|
||||
@@ -64,7 +63,7 @@ MIN_SLEEP = 0.015
|
||||
|
||||
### utility functions
|
||||
def NSURL_to_path(url):
|
||||
""" Convert URL string as represented by NSURL to a path string """
|
||||
"""Convert URL string as represented by NSURL to a path string"""
|
||||
nsurl = Foundation.NSURL.alloc().initWithString_(
|
||||
Foundation.NSString.alloc().initWithString_(str(url))
|
||||
)
|
||||
@@ -74,7 +73,7 @@ def NSURL_to_path(url):
|
||||
|
||||
|
||||
def path_to_NSURL(path):
|
||||
""" Convert path string to NSURL """
|
||||
"""Convert path string to NSURL"""
|
||||
pathstr = Foundation.NSString.alloc().initWithString_(str(path))
|
||||
url = Foundation.NSURL.fileURLWithPath_(pathstr)
|
||||
pathstr.dealloc()
|
||||
@@ -82,10 +81,10 @@ def path_to_NSURL(path):
|
||||
|
||||
|
||||
def check_photokit_authorization():
|
||||
""" Check authorization to use user's Photos Library
|
||||
"""Check authorization to use user's Photos Library
|
||||
|
||||
Returns:
|
||||
True if user has authorized access to the Photos library, otherwise False
|
||||
True if user has authorized access to the Photos library, otherwise False
|
||||
"""
|
||||
|
||||
auth_status = Photos.PHPhotoLibrary.authorizationStatus()
|
||||
@@ -93,7 +92,7 @@ def check_photokit_authorization():
|
||||
|
||||
|
||||
def request_photokit_authorization():
|
||||
""" Request authorization to user's Photos Library
|
||||
"""Request authorization to user's Photos Library
|
||||
|
||||
Returns:
|
||||
authorization status
|
||||
@@ -135,39 +134,39 @@ def request_photokit_authorization():
|
||||
|
||||
### exceptions
|
||||
class PhotoKitError(Exception):
|
||||
"""Base class for exceptions in this module. """
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PhotoKitFetchFailed(PhotoKitError):
|
||||
"""Exception raised for errors in the input. """
|
||||
"""Exception raised for errors in the input."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PhotoKitAuthError(PhotoKitError):
|
||||
"""Exception raised if unable to authorize use of PhotoKit. """
|
||||
"""Exception raised if unable to authorize use of PhotoKit."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PhotoKitExportError(PhotoKitError):
|
||||
"""Exception raised if unable to export asset. """
|
||||
"""Exception raised if unable to export asset."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PhotoKitMediaTypeError(PhotoKitError):
|
||||
""" Exception raised if an unknown mediaType() is encountered """
|
||||
"""Exception raised if an unknown mediaType() is encountered"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
### helper classes
|
||||
class ImageData:
|
||||
""" Simple class to hold the data passed to the handler for
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
"""Simple class to hold the data passed to the handler for
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -181,8 +180,7 @@ class ImageData:
|
||||
|
||||
|
||||
class AVAssetData:
|
||||
""" Simple class to hold the data passed to the handler for
|
||||
"""
|
||||
"""Simple class to hold the data passed to the handler for"""
|
||||
|
||||
def __init__(self):
|
||||
self.asset = None
|
||||
@@ -192,7 +190,7 @@ class AVAssetData:
|
||||
|
||||
|
||||
class PHAssetResourceData:
|
||||
""" Simple class to hold data from
|
||||
"""Simple class to hold data from
|
||||
requestDataForAssetResource:options:dataReceivedHandler:completionHandler:
|
||||
"""
|
||||
|
||||
@@ -200,19 +198,9 @@ class PHAssetResourceData:
|
||||
self.data = b""
|
||||
|
||||
|
||||
# class LivePhotoData:
|
||||
# """ Simple class to hold the data passed to the handler for
|
||||
# requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:
|
||||
# """
|
||||
|
||||
# def __init__(self):
|
||||
# self.live_photo = None
|
||||
# self.info = None
|
||||
|
||||
|
||||
class PhotoKitNotificationDelegate(NSObject):
|
||||
""" Handles notifications from NotificationCenter;
|
||||
used with asynchronous PhotoKit requests to stop event loop when complete
|
||||
"""Handles notifications from NotificationCenter;
|
||||
used with asynchronous PhotoKit requests to stop event loop when complete
|
||||
"""
|
||||
|
||||
def liveNotification_(self, note):
|
||||
@@ -226,11 +214,11 @@ class PhotoKitNotificationDelegate(NSObject):
|
||||
|
||||
### main class implementation
|
||||
class PhotoAsset:
|
||||
""" PhotoKit PHAsset representation """
|
||||
"""PhotoKit PHAsset representation"""
|
||||
|
||||
def __init__(self, manager, phasset):
|
||||
""" Return a PhotoAsset object
|
||||
|
||||
"""Return a PhotoAsset object
|
||||
|
||||
Args:
|
||||
manager = ImageManager object
|
||||
phasset: a PHAsset object
|
||||
@@ -241,32 +229,32 @@ class PhotoAsset:
|
||||
|
||||
@property
|
||||
def phasset(self):
|
||||
""" Return PHAsset instance """
|
||||
"""Return PHAsset instance"""
|
||||
return self._phasset
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" Return local identifier (UUID) of PHAsset """
|
||||
"""Return local identifier (UUID) of PHAsset"""
|
||||
return self._phasset.localIdentifier()
|
||||
|
||||
@property
|
||||
def isphoto(self):
|
||||
""" Return True if asset is photo (image), otherwise False """
|
||||
"""Return True if asset is photo (image), otherwise False"""
|
||||
return self.media_type == Photos.PHAssetMediaTypeImage
|
||||
|
||||
@property
|
||||
def ismovie(self):
|
||||
""" Return True if asset is movie (video), otherwise False """
|
||||
"""Return True if asset is movie (video), otherwise False"""
|
||||
return self.media_type == Photos.PHAssetMediaTypeVideo
|
||||
|
||||
@property
|
||||
def isaudio(self):
|
||||
""" Return True if asset is audio, otherwise False """
|
||||
"""Return True if asset is audio, otherwise False"""
|
||||
return self.media_type == Photos.PHAssetMediaTypeAudio
|
||||
|
||||
@property
|
||||
def original_filename(self):
|
||||
""" Return original filename asset was imported with """
|
||||
"""Return original filename asset was imported with"""
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if (
|
||||
@@ -278,10 +266,22 @@ class PhotoAsset:
|
||||
return resource.originalFilename()
|
||||
return None
|
||||
|
||||
@property
|
||||
def raw_filename(self):
|
||||
"""Return RAW filename for RAW+JPEG photos or None if no RAW asset"""
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if (
|
||||
self.isphoto
|
||||
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
|
||||
):
|
||||
return resource.originalFilename()
|
||||
return None
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
""" Check to see if a PHAsset has adjustment data associated with it
|
||||
Returns False if no adjustments, True if any adjustments """
|
||||
"""Check to see if a PHAsset has adjustment data associated with it
|
||||
Returns False if no adjustments, True if any adjustments"""
|
||||
|
||||
# reference: https://developer.apple.com/documentation/photokit/phassetresource/1623988-assetresourcesforasset?language=objc
|
||||
|
||||
@@ -298,112 +298,112 @@ class PhotoAsset:
|
||||
|
||||
@property
|
||||
def media_type(self):
|
||||
""" media type such as image or video """
|
||||
"""media type such as image or video"""
|
||||
return self.phasset.mediaType()
|
||||
|
||||
@property
|
||||
def media_subtypes(self):
|
||||
""" media subtype """
|
||||
"""media subtype"""
|
||||
return self.phasset.mediaSubtypes()
|
||||
|
||||
@property
|
||||
def panorama(self):
|
||||
""" return True if asset is panorama, otherwise False """
|
||||
"""return True if asset is panorama, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoPanorama)
|
||||
|
||||
@property
|
||||
def hdr(self):
|
||||
""" return True if asset is HDR, otherwise False """
|
||||
"""return True if asset is HDR, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoHDR)
|
||||
|
||||
@property
|
||||
def screenshot(self):
|
||||
""" return True if asset is screenshot, otherwise False """
|
||||
"""return True if asset is screenshot, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoScreenshot)
|
||||
|
||||
@property
|
||||
def live(self):
|
||||
""" return True if asset is live, otherwise False """
|
||||
"""return True if asset is live, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoLive)
|
||||
|
||||
@property
|
||||
def streamed(self):
|
||||
""" return True if asset is streamed video, otherwise False """
|
||||
"""return True if asset is streamed video, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoStreamed)
|
||||
|
||||
@property
|
||||
def slow_mo(self):
|
||||
""" return True if asset is slow motion (high frame rate) video, otherwise False """
|
||||
"""return True if asset is slow motion (high frame rate) video, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoHighFrameRate)
|
||||
|
||||
@property
|
||||
def time_lapse(self):
|
||||
""" return True if asset is time lapse video, otherwise False """
|
||||
"""return True if asset is time lapse video, otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoTimelapse)
|
||||
|
||||
@property
|
||||
def portrait(self):
|
||||
""" return True if asset is portrait (depth effect), otherwise False """
|
||||
"""return True if asset is portrait (depth effect), otherwise False"""
|
||||
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoDepthEffect)
|
||||
|
||||
@property
|
||||
def burstid(self):
|
||||
""" return burstIdentifier of image if image is burst photo otherwise None """
|
||||
"""return burstIdentifier of image if image is burst photo otherwise None"""
|
||||
return self.phasset.burstIdentifier()
|
||||
|
||||
@property
|
||||
def burst(self):
|
||||
""" return True if image is burst otherwise False """
|
||||
"""return True if image is burst otherwise False"""
|
||||
return bool(self.burstid)
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
""" the means by which the asset entered the user's library """
|
||||
"""the means by which the asset entered the user's library"""
|
||||
return self.phasset.sourceType()
|
||||
|
||||
@property
|
||||
def pixel_width(self):
|
||||
""" width in pixels """
|
||||
"""width in pixels"""
|
||||
return self.phasset.pixelWidth()
|
||||
|
||||
@property
|
||||
def pixel_height(self):
|
||||
""" height in pixels """
|
||||
"""height in pixels"""
|
||||
return self.phasset.pixelHeight()
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
""" date asset was created """
|
||||
"""date asset was created"""
|
||||
return self.phasset.creationDate()
|
||||
|
||||
@property
|
||||
def date_modified(self):
|
||||
""" date asset was modified """
|
||||
"""date asset was modified"""
|
||||
return self.phasset.modificationDate()
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" location of the asset """
|
||||
"""location of the asset"""
|
||||
return self.phasset.location()
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
""" duration of the asset """
|
||||
"""duration of the asset"""
|
||||
return self.phasset.duration()
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
""" True if asset is favorite, otherwise False """
|
||||
"""True if asset is favorite, otherwise False"""
|
||||
return self.phasset.isFavorite()
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" True if asset is hidden, otherwise False """
|
||||
"""True if asset is hidden, otherwise False"""
|
||||
return self.phasset.isHidden()
|
||||
|
||||
def metadata(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return dict of asset metadata
|
||||
|
||||
"""Return dict of asset metadata
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
@@ -411,17 +411,28 @@ class PhotoAsset:
|
||||
return imagedata.metadata
|
||||
|
||||
def uti(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return UTI of asset
|
||||
|
||||
"""Return UTI of asset
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
imagedata = self._request_image_data(version=version)
|
||||
return imagedata.uti
|
||||
|
||||
def uti_raw(self):
|
||||
"""Return UTI of RAW component of RAW+JPEG pair"""
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if (
|
||||
self.isphoto
|
||||
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
|
||||
):
|
||||
return resource.uniformTypeIdentifier()
|
||||
return None
|
||||
|
||||
def url(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return URL of asset
|
||||
|
||||
"""Return URL of asset
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
@@ -429,8 +440,8 @@ class PhotoAsset:
|
||||
return str(imagedata.info["PHImageFileURLKey"])
|
||||
|
||||
def path(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return path of asset
|
||||
|
||||
"""Return path of asset
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
@@ -439,8 +450,8 @@ class PhotoAsset:
|
||||
return url.fileSystemRepresentation().decode("utf-8")
|
||||
|
||||
def orientation(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return orientation of asset
|
||||
|
||||
"""Return orientation of asset
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
@@ -449,8 +460,8 @@ class PhotoAsset:
|
||||
|
||||
@property
|
||||
def degraded(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" Return True if asset is degraded version
|
||||
|
||||
"""Return True if asset is degraded version
|
||||
|
||||
Args:
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
"""
|
||||
@@ -458,15 +469,23 @@ class PhotoAsset:
|
||||
return imagedata.info["PHImageResultIsDegradedKey"]
|
||||
|
||||
def export(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
self,
|
||||
dest,
|
||||
filename=None,
|
||||
version=PHOTOS_VERSION_CURRENT,
|
||||
overwrite=False,
|
||||
raw=False,
|
||||
**kwargs,
|
||||
):
|
||||
""" Export image to path
|
||||
"""Export image to path
|
||||
|
||||
Args:
|
||||
dest: str, path to destination directory
|
||||
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
raw: bool, if True, export RAW component of RAW+JPEG pair, default is False
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
List of path to exported image(s)
|
||||
@@ -475,9 +494,6 @@ class PhotoAsset:
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
|
||||
# if self.live:
|
||||
# raise NotImplementedError("Live photos not implemented yet")
|
||||
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
@@ -491,11 +507,28 @@ class PhotoAsset:
|
||||
|
||||
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")
|
||||
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
# will hold exported image data and needs to be cleaned up at end
|
||||
imagedata = None
|
||||
if raw:
|
||||
# export the raw component
|
||||
resources = self._resources()
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto:
|
||||
data = self._request_resource_data(resource)
|
||||
ext = pathlib.Path(self.raw_filename).suffix[1:]
|
||||
break
|
||||
else:
|
||||
raise PhotoKitExportError(
|
||||
"Could not get image data for RAW photo"
|
||||
)
|
||||
else:
|
||||
# TODO: if user has selected use RAW as original, this returns the RAW
|
||||
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
|
||||
imagedata = self._request_image_data(version=version)
|
||||
if not imagedata.image_data:
|
||||
raise PhotoKitExportError("Could not get image data")
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
data = imagedata.image_data
|
||||
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
|
||||
@@ -503,7 +536,9 @@ class PhotoAsset:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(imagedata.image_data)
|
||||
fd.write(data)
|
||||
|
||||
if imagedata:
|
||||
del imagedata
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
@@ -525,14 +560,14 @@ class PhotoAsset:
|
||||
return [str(output_file)]
|
||||
|
||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
""" Request image data and metadata for self._phasset
|
||||
|
||||
"""Request image data and metadata for self._phasset
|
||||
|
||||
Args:
|
||||
version: which version to request
|
||||
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
|
||||
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
|
||||
PHOTOS_VERSION_CURRENT, request current version with all edits
|
||||
PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version
|
||||
|
||||
|
||||
Returns:
|
||||
ImageData instance
|
||||
|
||||
@@ -562,14 +597,12 @@ class PhotoAsset:
|
||||
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) """
|
||||
"""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
|
||||
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
@@ -593,25 +626,67 @@ class PhotoAsset:
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
def _request_resource_data(self, resource):
|
||||
"""Request asset resource data (either photo or video component)
|
||||
|
||||
Args:
|
||||
resource: PHAssetResource to request
|
||||
|
||||
Raises:
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
|
||||
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)"""
|
||||
|
||||
nonlocal requestdata
|
||||
|
||||
requestdata.data += data
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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 _make_result_handle_(self, data):
|
||||
""" Make handler function and threading event to use with
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
data: Fetchdata class to hold resulting metadata
|
||||
returns: handler function, threading.Event() instance
|
||||
Following call to requestImageDataAndOrientationForAsset_options_resultHandler_,
|
||||
data will hold data from the fetch """
|
||||
"""Make handler function and threading event to use with
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
data: Fetchdata class to hold resulting metadata
|
||||
returns: handler function, threading.Event() instance
|
||||
Following call to requestImageDataAndOrientationForAsset_options_resultHandler_,
|
||||
data will hold data from the fetch"""
|
||||
|
||||
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) """
|
||||
"""result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object)"""
|
||||
|
||||
nonlocal data
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
data.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
@@ -626,14 +701,14 @@ class PhotoAsset:
|
||||
return handler, event
|
||||
|
||||
def _resources(self):
|
||||
""" Return list of PHAssetResource for object """
|
||||
"""Return list of PHAssetResource for object"""
|
||||
resources = Photos.PHAssetResource.assetResourcesForAsset_(self.phasset)
|
||||
return [resources.objectAtIndex_(idx) for idx in range(resources.count())]
|
||||
|
||||
|
||||
class SlowMoVideoExporter(NSObject):
|
||||
def initWithAVAsset_path_(self, avasset, path):
|
||||
""" init helper class for exporting slow-mo video
|
||||
"""init helper class for exporting slow-mo video
|
||||
|
||||
Args:
|
||||
avasset: AVAsset
|
||||
@@ -648,15 +723,17 @@ class SlowMoVideoExporter(NSObject):
|
||||
return self
|
||||
|
||||
def exportSlowMoVideo(self):
|
||||
""" export slow-mo video with AVAssetExportSession
|
||||
|
||||
"""export slow-mo video with AVAssetExportSession
|
||||
|
||||
Returns:
|
||||
path to exported file
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
exporter = (
|
||||
AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
)
|
||||
)
|
||||
exporter.setOutputURL_(self.url)
|
||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||
@@ -665,7 +742,7 @@ class SlowMoVideoExporter(NSObject):
|
||||
self.done = False
|
||||
|
||||
def handler():
|
||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||
"""result handler for exportAsynchronouslyWithCompletionHandler"""
|
||||
self.done = True
|
||||
|
||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||
@@ -695,11 +772,10 @@ class SlowMoVideoExporter(NSObject):
|
||||
self.url = None
|
||||
self.done = None
|
||||
self.nc = None
|
||||
# super(NSObject, self).dealloc()
|
||||
|
||||
|
||||
class VideoAsset(PhotoAsset):
|
||||
""" PhotoKit PHAsset representation of video asset """
|
||||
"""PhotoKit PHAsset representation of video asset"""
|
||||
|
||||
# TODO: doesn't work for slow-mo videos
|
||||
# see https://stackoverflow.com/questions/26152396/how-to-access-nsdata-nsurl-of-slow-motion-videos-using-photokit
|
||||
@@ -707,15 +783,21 @@ class VideoAsset(PhotoAsset):
|
||||
# https://developer.apple.com/documentation/photokit/phimagemanager/1616981-requestexportsessionforvideo?language=objc
|
||||
# above 10.15 only
|
||||
def export(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
self,
|
||||
dest,
|
||||
filename=None,
|
||||
version=PHOTOS_VERSION_CURRENT,
|
||||
overwrite=False,
|
||||
**kwargs,
|
||||
):
|
||||
""" Export video to path
|
||||
"""Export video to path
|
||||
|
||||
Args:
|
||||
dest: str, path to destination directory
|
||||
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
|
||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
List of path to exported image(s)
|
||||
@@ -765,7 +847,7 @@ class VideoAsset(PhotoAsset):
|
||||
def _export_slow_mo(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
):
|
||||
""" Export slow-motion video to path
|
||||
"""Export slow-motion video to path
|
||||
|
||||
Args:
|
||||
dest: str, path to destination directory
|
||||
@@ -814,14 +896,14 @@ class VideoAsset(PhotoAsset):
|
||||
|
||||
# todo: rewrite this with NotificationCenter and App event loop?
|
||||
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
""" Request video data for self._phasset
|
||||
|
||||
"""Request video data for self._phasset
|
||||
|
||||
Args:
|
||||
version: which version to request
|
||||
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
|
||||
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
|
||||
PHOTOS_VERSION_CURRENT, request current version with all edits
|
||||
PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError if passed invalid value for version
|
||||
"""
|
||||
@@ -843,7 +925,7 @@ class VideoAsset(PhotoAsset):
|
||||
event = threading.Event()
|
||||
|
||||
def handler(asset, audiomix, info):
|
||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||
"""result handler for requestAVAssetForVideo:asset options:options resultHandler"""
|
||||
nonlocal requestdata
|
||||
|
||||
requestdata.asset = asset
|
||||
@@ -865,8 +947,8 @@ class VideoAsset(PhotoAsset):
|
||||
|
||||
|
||||
class LivePhotoRequest(NSObject):
|
||||
""" Manage requests for live photo assets
|
||||
See: https://developer.apple.com/documentation/photokit/phimagemanager/1616984-requestlivephotoforasset?language=objc
|
||||
"""Manage requests for live photo assets
|
||||
See: https://developer.apple.com/documentation/photokit/phimagemanager/1616984-requestlivephotoforasset?language=objc
|
||||
"""
|
||||
|
||||
def initWithManager_Asset_(self, manager, asset):
|
||||
@@ -879,7 +961,7 @@ class LivePhotoRequest(NSObject):
|
||||
return self
|
||||
|
||||
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" return the photos and video components of a live video as [PHAssetResource] """
|
||||
"""return the photos and video components of a live video as [PHAssetResource]"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||
@@ -897,7 +979,7 @@ class LivePhotoRequest(NSObject):
|
||||
self.live_photo = None
|
||||
|
||||
def handler(result, info):
|
||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||
"""result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:"""
|
||||
if not info["PHImageResultIsDegradedKey"]:
|
||||
self.live_photo = result
|
||||
self.info = info
|
||||
@@ -939,7 +1021,7 @@ class LivePhotoRequest(NSObject):
|
||||
|
||||
|
||||
class LivePhotoAsset(PhotoAsset):
|
||||
""" Represents a live photo """
|
||||
"""Represents a live photo"""
|
||||
|
||||
def export(
|
||||
self,
|
||||
@@ -949,8 +1031,9 @@ class LivePhotoAsset(PhotoAsset):
|
||||
overwrite=False,
|
||||
photo=True,
|
||||
video=True,
|
||||
**kwargs,
|
||||
):
|
||||
""" Export image to path
|
||||
"""Export image to path
|
||||
|
||||
Args:
|
||||
dest: str, path to destination directory
|
||||
@@ -959,6 +1042,7 @@ class LivePhotoAsset(PhotoAsset):
|
||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||
photo: bool, if True, export photo component of live photo
|
||||
video: bool, if True, export live video component of live photo
|
||||
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
|
||||
|
||||
Returns:
|
||||
list of [path to exported image and/or video]
|
||||
@@ -1010,39 +1094,6 @@ class LivePhotoAsset(PhotoAsset):
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# 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 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))
|
||||
|
||||
# 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)
|
||||
@@ -1061,93 +1112,14 @@ class LivePhotoAsset(PhotoAsset):
|
||||
request.dealloc()
|
||||
return exported
|
||||
|
||||
def _request_resource_data(self, resource):
|
||||
""" Request asset resource data (either photo or video component)
|
||||
|
||||
Args:
|
||||
resource: PHAssetResource to request
|
||||
|
||||
Raises:
|
||||
"""
|
||||
|
||||
with objc.autorelease_pool():
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
|
||||
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) """
|
||||
|
||||
nonlocal requestdata
|
||||
|
||||
requestdata.data += data
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
# # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc
|
||||
|
||||
# # requestImageForAsset:targetSize:contentMode:options:resultHandler:
|
||||
|
||||
# options = Photos.PHImageRequestOptions.alloc().init()
|
||||
# options.setVersion_(version)
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# options.setSynchronous_(True)
|
||||
# options.setDeliveryMode_(
|
||||
# Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
# )
|
||||
|
||||
# event = threading.Event()
|
||||
# image_data = ImageData()
|
||||
|
||||
# def handler(result, info):
|
||||
# nonlocal image_data
|
||||
# if not info["PHImageResultIsDegradedKey"]:
|
||||
# image_data.image_data = result
|
||||
# image_data.info = info
|
||||
# event.set()
|
||||
|
||||
# self._manager.requestImageForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
# self._phasset,
|
||||
# Photos.PHImageManagerMaximumSize,
|
||||
# Photos.PHImageContentModeDefault,
|
||||
# options,
|
||||
# handler,
|
||||
# )
|
||||
# event.wait()
|
||||
# options.dealloc()
|
||||
# return image_data
|
||||
|
||||
|
||||
class PhotoLibrary:
|
||||
""" Interface to PhotoKit PHImageManager and PHPhotoLibrary """
|
||||
"""Interface to PhotoKit PHImageManager and PHPhotoLibrary"""
|
||||
|
||||
def __init__(self):
|
||||
""" Initialize ImageManager instance. Requests authorization to use the
|
||||
"""Initialize ImageManager instance. Requests authorization to use the
|
||||
Photos library if authorization has not already been granted.
|
||||
|
||||
|
||||
Raises:
|
||||
PhotoKitAuthError if unable to authorize access to PhotoKit
|
||||
"""
|
||||
@@ -1166,7 +1138,7 @@ class PhotoLibrary:
|
||||
self._phimagemanager = Photos.PHCachingImageManager.defaultManager()
|
||||
|
||||
def request_authorization(self):
|
||||
""" Request authorization to user's Photos Library
|
||||
"""Request authorization to user's Photos Library
|
||||
|
||||
Returns:
|
||||
authorization status
|
||||
@@ -1176,7 +1148,7 @@ class PhotoLibrary:
|
||||
return self.auth_status
|
||||
|
||||
def fetch_uuid_list(self, uuid_list):
|
||||
""" fetch PHAssets with uuids in uuid_list
|
||||
"""fetch PHAssets with uuids in uuid_list
|
||||
|
||||
Args:
|
||||
uuid_list: list of str (UUID of image assets to fetch)
|
||||
@@ -1205,7 +1177,7 @@ class PhotoLibrary:
|
||||
)
|
||||
|
||||
def fetch_uuid(self, uuid):
|
||||
""" fetch PHAsset with uuid = uuid
|
||||
"""fetch PHAsset with uuid = uuid
|
||||
|
||||
Args:
|
||||
uuid: str; UUID of image asset to fetch
|
||||
@@ -1223,11 +1195,11 @@ class PhotoLibrary:
|
||||
raise PhotoKitFetchFailed(f"Fetch did not return result for uuid {uuid}")
|
||||
|
||||
def fetch_burst_uuid(self, burstid, all=False):
|
||||
""" fetch PhotoAssets with burst ID = burstid
|
||||
|
||||
"""fetch PhotoAssets with burst ID = burstid
|
||||
|
||||
Args:
|
||||
burstid: str, burst UUID
|
||||
all: return all burst assets; if False returns only those selected by the user
|
||||
all: return all burst assets; if False returns only those selected by the user (including the "key photo" even if user hasn't manually selected it)
|
||||
|
||||
Returns:
|
||||
list of PhotoAsset objects
|
||||
@@ -1253,11 +1225,11 @@ class PhotoLibrary:
|
||||
)
|
||||
|
||||
def _asset_factory(self, phasset):
|
||||
""" creates a PhotoAsset, VideoAsset, or LivePhotoAsset
|
||||
"""creates a PhotoAsset, VideoAsset, or LivePhotoAsset
|
||||
|
||||
Args:
|
||||
phasset: PHAsset object
|
||||
|
||||
phasset: PHAsset object
|
||||
|
||||
Returns:
|
||||
PhotoAsset, VideoAsset, or LivePhotoAsset depending on type of PHAsset
|
||||
"""
|
||||
|
||||
45
osxphotos/photosalbum.py
Normal file
45
osxphotos/photosalbum.py
Normal file
@@ -0,0 +1,45 @@
|
||||
""" PhotosAlbum class to create an album in default Photos library and add photos to it """
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import photoscript
|
||||
from more_itertools import chunked
|
||||
|
||||
from .photoinfo import PhotoInfo
|
||||
from .utils import noop
|
||||
|
||||
|
||||
class PhotosAlbum:
|
||||
def __init__(self, name: str, verbose: Optional[callable] = None):
|
||||
self.name = name
|
||||
self.verbose = verbose or noop
|
||||
self.library = photoscript.PhotosLibrary()
|
||||
|
||||
album = self.library.album(name)
|
||||
if album is None:
|
||||
self.verbose(f"Creating Photos album '{self.name}'")
|
||||
album = self.library.create_album(name)
|
||||
self.album = album
|
||||
|
||||
def add(self, photo: PhotoInfo):
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
self.album.add([photo_])
|
||||
self.verbose(
|
||||
f"Added {photo.original_filename} ({photo.uuid}) to album {self.name}"
|
||||
)
|
||||
|
||||
def add_list(self, photo_list: List[PhotoInfo]):
|
||||
photos = []
|
||||
for p in photo_list:
|
||||
try:
|
||||
photos.append(photoscript.Photo(p.uuid))
|
||||
except Exception as e:
|
||||
self.verbose(f"Error creating Photo object for photo {p.uuid}: {e}")
|
||||
for photolist in chunked(photos, 10):
|
||||
self.album.add(photolist)
|
||||
photo_len = len(photos)
|
||||
photo_word = "photos" if photo_len > 1 else "photo"
|
||||
self.verbose(f"Added {photo_len} {photo_word} to album {self.name}")
|
||||
|
||||
def photos(self):
|
||||
return self.album.photos()
|
||||
@@ -70,12 +70,24 @@ def _process_comments_5(photosdb):
|
||||
results = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT
|
||||
ZINVITEEHASHEDPERSONID,
|
||||
ZINVITEEFIRSTNAME,
|
||||
ZINVITEELASTNAME,
|
||||
ZINVITEEFULLNAME
|
||||
FROM
|
||||
ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||
ZINVITEEHASHEDPERSONID AS HASHEDPERSONID,
|
||||
ZINVITEEFIRSTNAME AS FIRSTNAME,
|
||||
ZINVITEELASTNAME AS LASTNAME,
|
||||
ZINVITEEFULLNAME AS FULLNAME
|
||||
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||
WHERE HASHEDPERSONID IS NOT NULL
|
||||
AND HASHEDPERSONID != ""
|
||||
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
|
||||
UNION
|
||||
SELECT DISTINCT
|
||||
ZCLOUDOWNERHASHEDPERSONID AS HASHEDPERSONID,
|
||||
ZCLOUDOWNERFIRSTNAME AS FIRSTNAME,
|
||||
ZCLOUDOWNERLASTNAME AS LASTNAME,
|
||||
ZCLOUDOWNERFULLNAME AS FULLNAME
|
||||
FROM ZGENERICALBUM
|
||||
WHERE HASHEDPERSONID IS NOT NULL
|
||||
AND HASHEDPERSONID != ""
|
||||
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -148,10 +160,10 @@ def _process_comments_5(photosdb):
|
||||
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
|
||||
|
||||
# sort results
|
||||
for uuid in photosdb._db_comments_uuid:
|
||||
for uuid, value in photosdb._db_comments_uuid.items():
|
||||
if photosdb._db_comments_uuid[uuid]["likes"]:
|
||||
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
|
||||
if photosdb._db_comments_uuid[uuid]["comments"]:
|
||||
photosdb._db_comments_uuid[uuid]["comments"].sort(key=lambda x: x.datetime)
|
||||
value["comments"].sort(key=lambda x: x.datetime)
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -146,7 +146,6 @@ def _process_faceinfo_4(photosdb):
|
||||
|
||||
# Photos 5 only
|
||||
face["agetype"] = None
|
||||
face["baldtype"] = None
|
||||
face["eyemakeuptype"] = None
|
||||
face["eyestate"] = None
|
||||
face["facialhairtype"] = None
|
||||
@@ -194,7 +193,7 @@ def _process_faceinfo_5(photosdb):
|
||||
ZDETECTEDFACE.ZPERSON,
|
||||
ZPERSON.ZFULLNAME,
|
||||
ZDETECTEDFACE.ZAGETYPE,
|
||||
ZDETECTEDFACE.ZBALDTYPE,
|
||||
NULL, -- ZDETECTEDFACE.ZBALDTYPE (Removed in Monterey)
|
||||
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||
ZDETECTEDFACE.ZEYESSTATE,
|
||||
ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||
@@ -239,7 +238,7 @@ def _process_faceinfo_5(photosdb):
|
||||
# 3 ZDETECTEDFACE.ZPERSON,
|
||||
# 4 ZPERSON.ZFULLNAME,
|
||||
# 5 ZDETECTEDFACE.ZAGETYPE,
|
||||
# 6 ZDETECTEDFACE.ZBALDTYPE,
|
||||
# 6 ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
|
||||
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||
# 8 ZDETECTEDFACE.ZEYESSTATE,
|
||||
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||
@@ -284,7 +283,6 @@ def _process_faceinfo_5(photosdb):
|
||||
face["person"] = person_pk
|
||||
face["fullname"] = normalize_unicode(row[4])
|
||||
face["agetype"] = row[5]
|
||||
face["baldtype"] = row[6]
|
||||
face["eyemakeuptype"] = row[7]
|
||||
face["eyestate"] = row[8]
|
||||
face["facialhairtype"] = row[9]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,13 @@ import logging
|
||||
import plistlib
|
||||
|
||||
from .._constants import (
|
||||
_PHOTOS_2_VERSION,
|
||||
_PHOTOS_3_VERSION,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_MODEL_VERSION,
|
||||
_PHOTOS_5_VERSION,
|
||||
_PHOTOS_6_MODEL_VERSION,
|
||||
_PHOTOS_7_MODEL_VERSION,
|
||||
_TESTED_DB_VERSIONS,
|
||||
)
|
||||
from ..utils import _open_sql_file
|
||||
@@ -73,12 +78,41 @@ def get_db_model_version(db_file):
|
||||
|
||||
model_ver = get_model_version(db_file)
|
||||
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
|
||||
db_ver = 5
|
||||
return 5
|
||||
elif _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
|
||||
db_ver = 6
|
||||
return 6
|
||||
elif _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
|
||||
return 7
|
||||
else:
|
||||
logging.warning(f"Unknown model version: {model_ver}")
|
||||
# cross our fingers and try latest version
|
||||
db_ver = 6
|
||||
return 7
|
||||
|
||||
return db_ver
|
||||
|
||||
class UnknownLibraryVersion(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_photos_library_version(library_path):
|
||||
"""Return int indicating which Photos version a library was created with """
|
||||
library_path = pathlib.Path(library_path)
|
||||
db_ver = get_db_version(str(library_path / "database" / "photos.db"))
|
||||
db_ver = int(db_ver)
|
||||
if db_ver == int(_PHOTOS_2_VERSION):
|
||||
return 2
|
||||
if db_ver == int(_PHOTOS_3_VERSION):
|
||||
return 3
|
||||
if db_ver == int(_PHOTOS_4_VERSION):
|
||||
return 4
|
||||
if db_ver != int(_PHOTOS_5_VERSION):
|
||||
raise UnknownLibraryVersion(f"db_ver = {db_ver}")
|
||||
|
||||
model_ver = get_model_version(str(library_path / "database" / "Photos.sqlite"))
|
||||
model_ver = int(model_ver)
|
||||
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
|
||||
return 5
|
||||
if _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
|
||||
return 6
|
||||
if _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
|
||||
return 7
|
||||
raise UnknownLibraryVersion(f"db_ver = {db_ver}, model_ver = {model_ver}")
|
||||
|
||||
130
osxphotos/phototemplate.md
Normal file
130
osxphotos/phototemplate.md
Normal file
@@ -0,0 +1,130 @@
|
||||
The templating system converts one or template statements, written in osxphotos metadata templating language, to one or more rendered values using information from the photo being processed.
|
||||
|
||||
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
|
||||
|
||||
Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
|
||||
|
||||
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||
|
||||
`+`: If present before template `name`, expands the template in place. If `delim` not provided, values are joined with no delimiter.
|
||||
|
||||
e.g. if Photo keywords are `["foo","bar"]`:
|
||||
|
||||
- `"{keyword}"` renders to `"foo", "bar"`
|
||||
- `"{,+keyword}"` renders to: `"foo,bar"`
|
||||
- `"{; +keyword}"` renders to: `"foo; bar"`
|
||||
- `"{+keyword}"` renders to `"foobar"`
|
||||
|
||||
`template_field`: The template field to resolve. See [Template Substitutions](#template-substitutions) for full list of template fields.
|
||||
|
||||
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
|
||||
|
||||
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
|
||||
|
||||
Valid filters are:
|
||||
|
||||
<!-- OSXPHOTOS-FILTER-TABLE:START - Do not remove or modify this section -->
|
||||
- lower: Convert value to lower case, e.g. 'Value' => 'value'.
|
||||
- upper: Convert value to upper case, e.g. 'Value' => 'VALUE'.
|
||||
- strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
|
||||
- titlecase: Convert value to title case, e.g. 'my value' => 'My Value'.
|
||||
- capitalize: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
|
||||
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||
- shell_quote: Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.
|
||||
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||
|
||||
e.g. if Photo keywords are `["FOO","bar"]`:
|
||||
|
||||
- `"{keyword|lower}"` renders to `"foo", "bar"`
|
||||
- `"{keyword|upper}"` renders to: `"FOO", "BAR"`
|
||||
- `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
|
||||
- `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
|
||||
|
||||
e.g. if Photo description is "my description":
|
||||
|
||||
- `"{descr|titlecase}"` renders to: `"My Description"`
|
||||
|
||||
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
|
||||
|
||||
e.g. If Photo is in `Album1` in `Folder1`:
|
||||
|
||||
- `"{folder_album}"` renders to `["Folder1/Album1"]`
|
||||
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||
|
||||
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
- `contains`: template field contains value, similar to python's `in`
|
||||
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
|
||||
- `startswith`: template field starts with value
|
||||
- `endswith`: template field ends with value
|
||||
- `<=`: template field is less than or equal to value
|
||||
- `>=`: template field is greater than or equal to value
|
||||
- `<`: template field is less than value
|
||||
- `>`: template field is greater than value
|
||||
- `==`: template field equals value
|
||||
- `!=`: template field does not equal value
|
||||
|
||||
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
|
||||
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
|
||||
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
|
||||
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
|
||||
|
||||
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
|
||||
|
||||
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example:
|
||||
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
- `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
|
||||
|
||||
and if it is not an HDR image,
|
||||
|
||||
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
|
||||
|
||||
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
|
||||
|
||||
e.g., if photo has no title set,
|
||||
|
||||
- `"{title}"` renders to "_"
|
||||
- `"{title,I have no title}"` renders to `"I have no title"`
|
||||
|
||||
Template fields such as `created.strftime` use the default value to pass the template to use for `strftime`.
|
||||
|
||||
e.g., if photo date is 4 February 2020, 19:07:38,
|
||||
|
||||
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
|
||||
|
||||
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 was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
|
||||
|
||||
Either or both bool_value or default (False value) may be empty which would result in empty string `""` when rendered.
|
||||
|
||||
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
|
||||
|
||||
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user