Browse Source

Map and photo view skeleton

api
dave 3 years ago
parent
commit
e10ff6b2c5
  1. 153
      package-lock.json
  2. 25
      photoapp/daemon.py
  3. 19
      photoapp/image.py
  4. 12
      photoapp/library.py
  5. 1
      photoapp/types.py
  6. 4
      styles/dist/unknown.svg
  7. 16
      styles/less/main.less
  8. 1
      templates/feed.html
  9. 33
      templates/map.html
  10. 1
      templates/monthly.html
  11. 2
      templates/page-top.html
  12. 54
      templates/photo.html

153
package-lock.json

@ -11,7 +11,6 @@
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"optional": true,
"requires": {
"co": "^4.6.0",
"fast-deep-equal": "^1.0.0",
@ -55,14 +54,12 @@
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"optional": true
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"optional": true,
"requires": {
"safer-buffer": "~2.1.0"
}
@ -80,20 +77,17 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"optional": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
"optional": true
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"optional": true
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"balanced-match": {
"version": "1.0.0",
@ -104,7 +98,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"optional": true,
"requires": {
"tweetnacl": "^0.14.3"
}
@ -156,8 +149,7 @@
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"optional": true
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"chalk": {
"version": "2.4.1",
@ -192,8 +184,7 @@
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"optional": true
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
},
"coffeescript": {
"version": "1.10.0",
@ -239,8 +230,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"optional": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"currently-unhandled": {
"version": "0.4.1",
@ -254,7 +244,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"optional": true,
"requires": {
"assert-plus": "^1.0.0"
}
@ -295,7 +284,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"optional": true,
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
@ -305,7 +293,6 @@
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"optional": true,
"requires": {
"prr": "~1.0.1"
}
@ -350,8 +337,7 @@
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"optional": true
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extsprintf": {
"version": "1.3.0",
@ -361,14 +347,12 @@
"fast-deep-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
"optional": true
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
"optional": true
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
"faye-websocket": {
"version": "0.10.0",
@ -421,14 +405,12 @@
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
"optional": true
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
"optional": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "1.0.6",
@ -462,7 +444,6 @@
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"optional": true,
"requires": {
"assert-plus": "^1.0.0"
}
@ -704,14 +685,12 @@
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
"optional": true
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
"integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
"optional": true,
"requires": {
"ajv": "^5.3.0",
"har-schema": "^2.0.0"
@ -749,7 +728,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"optional": true,
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
@ -764,11 +742,15 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
"integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA=="
},
"image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
"optional": true
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w="
},
"indent-string": {
"version": "2.1.0",
@ -816,8 +798,7 @@
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"optional": true
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"is-utf8": {
"version": "0.2.1",
@ -832,8 +813,7 @@
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"optional": true
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"js-yaml": {
"version": "3.5.5",
@ -847,32 +827,27 @@
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"optional": true
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"optional": true
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"optional": true
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"optional": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
@ -995,8 +970,7 @@
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.36.0",
@ -1071,14 +1045,23 @@
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"optional": true
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"ol": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ol/-/ol-5.2.0.tgz",
"integrity": "sha512-/sXlXKMT7W/4txYmEJiqZousQ5GScDoqVrltyQDl0himUD1oEV460bH74eD49AMm0OmQL9+TIn6DXKqOYzjTLw==",
"requires": {
"pbf": "3.1.0",
"pixelworks": "1.1.0",
"rbush": "2.0.2"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1118,11 +1101,19 @@
"pinkie-promise": "^2.0.0"
}
},
"pbf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.1.0.tgz",
"integrity": "sha512-/hYJmIsTmh7fMkHAWWXJ5b8IKLWdjdlAFb3IHkRBn1XUhIYBChVGfVwmHEAV3UfXTxsP/AKfYTXTS/dCPxJd5w==",
"requires": {
"ieee754": "^1.1.6",
"resolve-protobuf-schema": "^2.0.0"
}
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"optional": true
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"pify": {
"version": "2.3.0",
@ -1142,6 +1133,11 @@
"pinkie": "^2.0.0"
}
},
"pixelworks": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pixelworks/-/pixelworks-1.1.0.tgz",
"integrity": "sha1-Hwla1I3Ki/ihyCWOAJIDGkTyLKU="
},
"pretty-bytes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz",
@ -1154,28 +1150,29 @@
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"optional": true,
"requires": {
"asap": "~2.0.3"
}
},
"protocol-buffers-schema": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz",
"integrity": "sha512-Xdayp8sB/mU+sUV4G7ws8xtYMGdQnxbeIfLjyO9TZZRJdztBGhlmbI5x1qcY4TG5hBkIKGnc28i7nXxaugu88w=="
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"optional": true
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
},
"psl": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
"optional": true
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"optional": true
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"purecss": {
"version": "1.0.0",
@ -1187,6 +1184,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"quickselect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz",
"integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ=="
},
"raw-body": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
@ -1196,6 +1198,14 @@
"string_decoder": "0.10"
}
},
"rbush": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz",
"integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==",
"requires": {
"quickselect": "^1.0.1"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@ -1236,7 +1246,6 @@
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"optional": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@ -1265,6 +1274,14 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
"integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
},
"resolve-protobuf-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"requires": {
"protocol-buffers-schema": "^3.3.1"
}
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
@ -1340,7 +1357,6 @@
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
"optional": true,
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
@ -1412,7 +1428,6 @@
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"optional": true,
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
@ -1427,7 +1442,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"optional": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@ -1435,8 +1449,7 @@
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"optional": true
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"underscore.string": {
"version": "3.3.4",
@ -1455,8 +1468,7 @@
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"optional": true
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"validate-npm-package-license": {
"version": "3.0.4",
@ -1471,7 +1483,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"optional": true,
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",

25
photoapp/daemon.py

@ -13,6 +13,7 @@ class PhotosWeb(object):
self.tpl = Environment(loader=FileSystemLoader('templates'),
autoescape=select_autoescape(['html', 'xml']))
self.tpl.globals.update(mime2ext=self.mime2ext)
self.tpl.filters['basename'] = os.path.basename
self.thumb = ThumbnailView(self)
self.photo = PhotoView(self)
self.download = DownloadView(self)
@ -50,6 +51,14 @@ class PhotosWeb(object):
yield self.tpl.get_template("monthly.html").render(images=images)
@cherrypy.expose
def map(self, i=None, zoom=3):
s = self.session()
query = s.query(PhotoSet).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
if i:
query = query.filter(PhotoSet.uuid == i)
yield self.tpl.get_template("map.html").render(images=query.all(), zoom=int(zoom))
@cherrypy.popargs('item_type', 'thumb_size', 'uuid')
class ThumbnailView(object):
@ -81,9 +90,9 @@ class ThumbnailView(object):
# TODO some lock around calls to this based on uuid
thumb_path = self.master.library.make_thumb(thumb_from, thumb_size)
if thumb_path:
return cherrypy.lib.static.serve_file(thumb_path, 'image/jpeg')
return cherrypy.lib.static.serve_file(thumb_path, "image/jpeg")
else:
return "No thumbnail available" # TODO generic svg icon
return cherrypy.lib.static.serve_file(os.path.abspath("styles/dist/unknown.svg"), "image/svg+xml")
@cherrypy.popargs('item_type', 'uuid')
@ -164,17 +173,17 @@ def main():
"tools.staticdir.dir": os.path.abspath("./styles/dist")}})
cherrypy.config.update({
'sessionFilter.on': True,
'tools.sessions.on': True,
'tools.sessions.locking': 'explicit',
'tools.sessions.timeout': 525600,
'tools.gzip.on': True,
# 'sessionFilter.on': True,
# 'tools.sessions.on': True,
# 'tools.sessions.locking': 'explicit',
# 'tools.sessions.timeout': 525600,
# 'tools.gzip.on': True,
'request.show_tracebacks': True,
'server.socket_port': args.port,
'server.thread_pool': 25,
'server.socket_host': '0.0.0.0',
'server.show_tracebacks': True,
'server.socket_timeout': 5,
# 'server.socket_timeout': 5,
'log.screen': False,
'engine.autoreload.on': args.debug
})

19
photoapp/image.py

@ -11,7 +11,7 @@ def get_jpg_info(fpath):
"""
Given the path to a jpg, return a dict describing it
"""
date, gps, dimensions = get_exif_data(fpath)
date, gps, dimensions, orientation = get_exif_data(fpath)
if date is None:
import pdb
@ -24,7 +24,8 @@ def get_jpg_info(fpath):
mime = magic.from_file(fpath, mime=True)
size = os.path.getsize(fpath)
photo = Photo(hash=get_hash(fpath), path=fpath, format=mime, size=size, width=dimensions[0], height=dimensions[1])
photo = Photo(hash=get_hash(fpath), path=fpath, format=mime, size=size,
width=dimensions[0], height=dimensions[1], orientation=orientation)
return PhotoSet(date=date, lat=lat, lon=lon, files=[photo])
@ -45,13 +46,14 @@ def get_hash(path):
def get_exif_data(path):
"""
Return a (datetime, (decimal, decimal)) tuple describing the photo's exif date and gps coordinates
Return a (datetime, (decimal, decimal), (width, height), rotation) tuple describing the photo's exif date and gps coordinates
"""
img = Image.open(path)
datestr = None
gpsinfo = None
dateinfo = None
orientationinfo = 0
sizeinfo = (img.width, img.height)
if img.format in ["JPEG", "PNG", "GIF"]:
@ -74,6 +76,10 @@ def get_exif_data(path):
raise Exception("{} has no DateTime".format(path)) # TODO how often do we hit this
dateinfo = datetime.strptime(datestr, "%Y:%m:%d %H:%M:%S")
orien = exif.get("Orientation")
if orien:
orientationinfo = {0: 0, 8: 1, 3: 2, 6: 3}.get(int(orien), 0)
gps = exif.get("GPSInfo")
if gps:
# see https://gis.stackexchange.com/a/273402
@ -84,16 +90,11 @@ def get_exif_data(path):
if gps[3] == 'W':
gps_x *= -1
gpsinfo = (gps_y, gps_x)
import pdb
pdb.set_trace()
pass
pass
pass
if dateinfo is None:
dateinfo = get_mtime(path)
return dateinfo, gpsinfo, sizeinfo
return dateinfo, gpsinfo, sizeinfo, orientationinfo
def rational64u_to_hms(values):

12
photoapp/library.py

@ -70,16 +70,20 @@ class PhotoLibrary(object):
Create a thumbnail of the given photo, scaled/cropped to the given named style
:return: local path to thumbnail file or None if creation failed or was blocked
"""
styles = {"feed": (250, 250),
styles = {"tiny": (80, 80),
"small": (100, 100),
"feed": (250, 250),
"preview": (1024, 768),
"big": (2048, 1536)}
dest = os.path.join(self.cache_path, "thumbs", style, "{}.jpg".format(photo.uuid))
if os.path.exists(dest):
return os.path.abspath(dest)
if photo.width is None: # todo better detection of images that PIL can't open
return None
if photo.uuid not in self._failed_thumbs_cache[style]:
width = min(styles[style][0], photo.width if photo.width > 0 else 999999999)
height = min(styles[style][1], photo.height if photo.height > 0 else 999999999) # TODO this is bad.
p = Process(target=self.gen_thumb, args=(os.path.join(self.path, photo.path), dest, width, height))
p = Process(target=self.gen_thumb, args=(os.path.join(self.path, photo.path), dest, width, height, photo.orientation))
p.start()
p.join()
if p.exitcode != 0:
@ -89,13 +93,13 @@ class PhotoLibrary(object):
return None
@staticmethod
def gen_thumb(src_img, dest_img, width, height):
def gen_thumb(src_img, dest_img, width, height, rotation):
try:
start = time()
# TODO lock around the dir creation
os.makedirs(os.path.split(dest_img)[0], exist_ok=True)
image = Image.open(src_img)
image = image.rotate(90 * rotation, expand=True)
thumb = ImageOps.fit(image, (width, height), Image.ANTIALIAS)
thumb.save(dest_img, 'JPEG')
print("Generated {} in {}s".format(dest_img, round(time() - start, 4)))

1
photoapp/types.py

@ -32,6 +32,7 @@ class Photo(Base):
size = Column(Integer)
width = Column(Integer)
height = Column(Integer)
orientation = Column(Integer, default=0)
hash = Column(String(length=64), unique=True)
path = Column(Unicode)
format = Column(String(length=64)) # TODO how long can a mime string be

4
styles/dist/unknown.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 240 240">
<path stroke="none" d="M 200,100 100,200 0,100 100,0 200,100 z M 135.64709,74.70585 q 0,-13.52935 -10.00006,-22.52943 -9.99999,-8.99999 -24.35289,-8.99999 -17.29415,0 -30.117661,5.29409 L 69.05879,69.52938 q 9.764731,-6.23528 21.52944,-6.23528 8.82356,0 14.58824,4.82351 5.76469,4.82351 5.76469,12.70589 0,8.5883 -9.94117,21.70588 -9.94117,13.11766 -9.94117,26.76473 l 17.88236,0 q 0,-6.3529 6.9412,-14.9412 11.76471,-14.58816 12.82351,-16.35289 6.9412,-11.05887 6.9412,-23.29417 z m -22.00003,92.11771 0,-24.70585 -27.29412,0 0,24.70585 27.29412,0 z" />
</svg>

16
styles/less/main.less

@ -130,7 +130,7 @@ a {
}
.nav-inner {
display: block;
padding: 2em 0;
padding: 1.5em 0;
}
#nav .nav-menu-button {
display: none;
@ -183,7 +183,7 @@ a {
}
.photo-view {
img {
.photo-preview img {
display: block;
width: 100%;
max-height: 100%;
@ -192,4 +192,16 @@ a {
.photo-info {
padding: 0px 0px 0px 25px;
}
.photo-formats {
ul {
padding: 0;
}
li {
margin-bottom: 15px;
}
img {
// float: left;
max-width: 100px;
}
}
}

1
templates/feed.html

@ -1,4 +1,5 @@
{% set title = "All photos" %}
{% set subtitle = "By date, descending" %}
{% include "page-top.html" %}

33
templates/map.html

@ -0,0 +1,33 @@
{% set title = "Photo map" %}
{% set subtitle = "GPS data" %}
{% include "page-top.html" %}
<div class="photo-map">
<div id="mapdiv" style="height: 900px"></div>
<script src="http://www.openlayers.org/api/OpenLayers.js"></script>
<script>
<!-- https://wiki.openstreetmap.org/wiki/OpenLayers_Marker_Example -->
var points = [
{%- for item in images %}
[{{item.lon}}, {{item.lat}}],
{%- endfor %}
]
var map = new OpenLayers.Map("mapdiv");
map.addLayer(new OpenLayers.Layer.OSM());
var markers = new OpenLayers.Layer.Markers( "Markers" );
for(var i=0;i<points.length;i++) {
var point = points[i]
var lonLat = new OpenLayers.LonLat(point[0], point[1])
.transform(new OpenLayers.Projection("EPSG:4326"),
map.getProjectionObject());
var marker = new OpenLayers.Marker(lonLat)
markers.addMarker(marker);
}
map.addLayer(markers);
var zoom={{ zoom or 3 }};
map.setCenter(lonLat, zoom);
</script>
</div>
{% include "page-bottom.html" %}

1
templates/monthly.html

@ -1,4 +1,5 @@
{% set title = "Server statistics" %}
{% set subtitle = "Placeholder" %}
{% include "page-top.html" %}

2
templates/page-top.html

@ -35,7 +35,7 @@
<div class="pure-u-1-2">
<h1 class="email-content-title">{{ title }}</h1>
<p class="email-content-subtitle">
The subheading
{{ subtitle }}
</p>
</div>
<div class="email-content-controls pure-u-1-2">

54
templates/photo.html

@ -0,0 +1,54 @@
{% set title = "Placeholder Title" %}
{% set subtitle = image.uuid %}
{% include "page-top.html" %}
<div class="photo-view pure-g">
<div class="photo-preview pure-u-2-3">
<a href="/thumb/set/big/{{ image.uuid }}.jpg">
<img src="/thumb/set/preview/{{ image.uuid }}.jpg" />
</a>
</div>
<div class="photo-info pure-u-1-3">
<div class="photo-metadata">
<h2>Information</h2>
<ul>
<li><strong>Date:</strong> {{ image.date }} </li>
<li><strong>Versions:</strong> {{ image.files|length }}</li>
{% if image.lat != 0 %}
<li><strong>Coordinates:</strong> <a href="/map?zoom=13&i={{ image.uuid }}">{{ image.lat }}, {{ image.lon }}</a></li>
{% endif %}
</ul>
</div>
<div class="photo-formats">
<h2>Versions</h2>
<ul class="pure-g">
{% for img in image.files %}
<li class="pure-u-1 pure-g">
<a href="/thumb/one/big/{{ img.uuid }}.jpg" class="pure-g-1-4">
<img src="/thumb/one/small/{{ img.uuid }}.jpg" />
</a>
<div class="pure-u-3-4">
<div>
{{ img.uuid }}
</div>
<div>
{{ img.path | basename }}
</div>
<div>
{{ img.size | filesizeformat }} - {{ img.format }}
</div>
<div>
<a href="/download/one/{{ img.uuid }}">download</a>
<a href="/download/one/{{ img.uuid }}.{{ mime2ext(img.format) }}?preview=true">preview</a>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% include "page-bottom.html" %}
Loading…
Cancel
Save