Last active
December 20, 2018 11:06
-
-
Save SpotlightKid/be9bc4c08a9e1531287c89182f7931d8 to your computer and use it in GitHub Desktop.
Remote-controllable digital image frame for iOS/Pythonista
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!python2 | |
# -*- coding: utf-8 -*- | |
"""Remote-controllable digital image frame with built-in web server.""" | |
import logging | |
import os | |
import re | |
import sys | |
import threading | |
import time | |
import tempfile | |
from operator import attrgetter | |
from os.path import join, splitext | |
try: | |
from queue import Empty, Queue | |
except ImportError: | |
from Queue import Empty, Queue | |
from wsgiref.simple_server import make_server, WSGIRequestHandler | |
import photos | |
import ui | |
import bottle | |
m = re.match('(?P<maj>\d+)\.(?P<min>\d+)(\.(?P<patch>\d+))?', bottle.__version__) | |
bottle_version = tuple(int(g) for g in m.group('maj', 'min', 'patch') if g) | |
if sys.version_info[0] >= 3 and bottle_version < (0, 12, 7): | |
import warnings | |
warnings.warn("Bottle version (%s) too old. Browser upload may not work reliably." % | |
bottle.__version__) | |
log = logging.getLogger('ImageFrame') | |
ALBUM_TITLE = 'Image Frame' | |
HTML_HEADER = """\ | |
<html> | |
<head> | |
<title>{{title if defined('title') else 'Image Frame Upload'}}</title> | |
</head> | |
<body> | |
""" | |
HTML_FOOTER = """\ | |
</body> | |
</html> | |
""" | |
TMPL_UPLOAD = HTML_HEADER + """\ | |
<h1>Upload Image</h1> | |
<form action="/upload" method="post" enctype="multipart/form-data" accept="image/*"> | |
Select a file: <input type="file" name="file" /> | |
<br /> | |
<input type="checkbox" id="show" name="show" value="1" checked="checked"> | |
<label for="show">Show image after upload?</label><br /> | |
<input type="submit" value="{{submit if defined('submit') else 'Upload'}}" /> | |
</form> | |
% if defined('url'): | |
<p>Asset <a href="{{url}}">{{id}}</a> successfully created.</p> | |
% end | |
""" + HTML_FOOTER | |
class ImageFrameView(ui.View): | |
def __init__(self, queue, server, check_interval=0.2): | |
self.queue = queue | |
self.img = None | |
self.server = server | |
self.check_interval = check_interval | |
self.background_color = 'black' | |
ui.delay(self.check_queue, .1) | |
def check_queue(self, *args, **kw): | |
try: | |
event = self.queue.get_nowait() | |
except Empty: | |
pass | |
else: | |
if event is None: | |
self.quit() | |
else: | |
type, image = event | |
if type == 'file': | |
self.load_image_file(image) | |
elif type == 'asset': | |
self.load_image_asset(image) | |
ui.delay(self.check_queue, self.check_interval) | |
def load_image_asset(self, image): | |
try: | |
asset = photos.get_asset_with_local_id(image) | |
except ValueError: | |
log.error("Invalid asset: %s" % image) | |
else: | |
self.img = asset.get_ui_image() | |
self.set_needs_display() | |
def load_image_file(self, image): | |
try: | |
img = ui.Image.named(image) | |
if not img: | |
raise IOError("Image not loaded.") | |
except: | |
log.error("Invalid image: %s" % image) | |
else: | |
self.img = img | |
self.set_needs_display() | |
def quit(self, close=True): | |
self.server.stop() | |
self.server.join(timeout=3) | |
if self.server.is_alive(): | |
log.warning("Server thread not terminated after 3 seconds!") | |
ui.cancel_delays() | |
if close: | |
self.close() | |
def will_close(self): | |
self.quit(False) | |
def draw(self): | |
if self.img: | |
wi, hi = self.img.size | |
ws, hs = self.width, self.height | |
if wi > hi: | |
h1 = hi * (ws / wi) | |
self.img.draw(0, hs / 2 - h1 / 2, ws, h1) | |
else: | |
w1 = wi * (hs / hi) | |
self.img.draw(ws / 2 - w1 / 2, 0, w1, hs) | |
# def touch_ended(self, touch): | |
# # Called when a touch ends. | |
# self.quit() | |
class QuietServer(bottle.WSGIRefServer): | |
def run(self, handler): | |
if self.quiet: | |
base = self.options.get('handler_class', WSGIRequestHandler) | |
class QuietHandler(base): | |
def log_request(*args, **kw): | |
pass | |
self.options['handler_class'] = QuietHandler | |
self.srv = make_server(self.host, self.port, handler, **self.options) | |
self.srv.serve_forever(poll_interval=0.1) | |
class ServerThread(threading.Thread): | |
def __init__(self, app, host='0.0.0.0', port=8080, **kw): | |
super(ServerThread, self).__init__() | |
self.app = app | |
self.host = host | |
self.port = port | |
self.daemon = False | |
self.finished = threading.Event() | |
self.server = QuietServer(host=self.host, port=self.port, **kw) | |
def run(self, *args, **kw): | |
try: | |
bottle.run(self.app, server=self.server, quiet=True) | |
finally: | |
self.finished.set() | |
def stop(self): | |
if not self.finished.is_set(): | |
self.server.srv.shutdown() | |
for tries in range(1, 4): | |
log.debug("Waiting for server thread to shut down (tries: %i)..." % tries) | |
if self.finished.wait(timeout=1): | |
log.debug("Server shut down.") | |
break | |
else: | |
log.warning("Server has not shut down after 3 seconds!") | |
self.server.srv.server_close() | |
webapp = bottle.Bottle() | |
queue = Queue() | |
@webapp.error(404) | |
def error404(error): | |
return dict(status='ERR', msg="Nothing here, sorry.") | |
@webapp.get('/') | |
@bottle.view(TMPL_UPLOAD) | |
def index(**kwargs): | |
return kwargs | |
@webapp.get('/show/<name>') | |
def show(name): | |
queue.put(('file', name)) | |
log.debug("Queued named image '%s' for display.", name) | |
return dict(status='OK', msg="Image file queued.") | |
@webapp.get('/assets') | |
@webapp.get('/assets/') | |
def get_assets(): | |
assets = [] | |
coll = get_album(ALBUM_TITLE) | |
for asset in sorted(coll.assets, key=attrgetter('modification_date'), reverse=True): | |
assets.append(dict( | |
local_id=asset.local_id, | |
width=asset.pixel_width, | |
height=asset.pixel_height, | |
modified=asset.modification_date.strftime('%Y%m%dT%H%M%S') | |
)) | |
return dict(status='OK', assets=assets) | |
@webapp.put('/assets') | |
@webapp.put('/assets/') | |
def put_asset(): | |
tmpdir = tempfile.mkdtemp() | |
filepath = join(tmpdir, 'asset.%s' % bottle.request.query.get('type', 'jpg')) | |
with open(filepath, 'wb') as fp: | |
fp.write(bottle.request.body.read()) | |
try: | |
asset = add_asset(filepath) | |
finally: | |
os.remove(filepath) | |
os.rmdir(tmpdir) | |
scheme, host = bottle.request.urlparts[:2] | |
url = "%s://%s/assets/%s" % (scheme, host, asset.local_id) | |
log.debug("Asset url: %s", url) | |
return dict(status='OK', msg="Image asset added.", id=asset.local_id, url=url) | |
@webapp.get('/assets/<local_id:path>') | |
def set_asset(local_id): | |
queue.put(('asset', local_id)) | |
log.debug("Queued asset %s for display.", local_id) | |
return dict(status='OK', msg="Image asset queued.") | |
@webapp.get('/shutdown') | |
@webapp.get('/shutdown/') | |
def shutdown(): | |
queue.put(None) | |
return dict(status='OK', msg="Shutdown initiated.") | |
@webapp.post('/upload') | |
def upload(): | |
file = bottle.request.files.get('file') | |
if not file: | |
return dict(status='ERR', msg="No file upload found in request data.") | |
name, ext = splitext(file.filename) | |
if ext.lower() not in ('.png', '.jpg', '.jpeg', '.gif'): | |
return dict(status='ERR', msg="Filetype not allowed.") | |
tmpdir = tempfile.mkdtemp() | |
filepath = join(tmpdir, file.filename) | |
file.save(tmpdir) | |
log.debug("Saved file upload to '%s'.", filepath) | |
try: | |
asset = add_asset(filepath) | |
finally: | |
os.remove(filepath) | |
os.rmdir(tmpdir) | |
if bottle.request.forms.get('show'): | |
queue.put(('asset', asset.local_id)) | |
log.debug("Queued asset %s for display.", asset.local_id) | |
scheme, host = bottle.request.urlparts[:2] | |
url = "%s://%s/assets/%s" % (scheme, host, asset.local_id) | |
log.debug("Asset url: %s", url) | |
return index(id=asset.local_id, url=url) | |
@webapp.route('/upload', method='ANY') | |
def upload_redirect(): | |
bottle.redirect('/') | |
def add_asset(path, title=ALBUM_TITLE): | |
"""Add image to photo album 'Image Frame'.""" | |
try: | |
coll = get_album(title) | |
if not coll.can_add_assets: | |
raise OSError | |
except OSError: | |
msg = "Sorry, album '%s' does not allow adding images." % title | |
log.error(msg) | |
abort(401, msg) | |
else: | |
asset = photos.create_image_asset(path) | |
coll.add_assets([asset]) | |
log.debug("Added asset %s.", asset.local_id) | |
return asset | |
def get_album(title): | |
for coll in photos.get_albums(): | |
if coll.title == title: | |
break | |
else: | |
coll = photos.create_album(title) | |
return coll | |
def main(args=[]): | |
logging.basicConfig(level=logging.DEBUG if '-v' in args else logging.INFO) | |
server = ServerThread(webapp) | |
view = ImageFrameView(queue, server) | |
server.start() | |
time.sleep(0.1) | |
if server.finished.is_set(): | |
log.error("Could not start web server.") | |
server.join(timeout=3) | |
else: | |
queue.put(('file', 'test:Lenna')) | |
view.present('fullscreen', hide_title_bar=True) | |
if __name__ == '__main__': | |
sys.exit(main(sys.argv[1:]) or 0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
"""Upload image to the digital image frame.""" | |
from __future__ import print_function, unicode_literals | |
import argparse | |
import logging | |
import os | |
import sys | |
from urllib.parse import quote | |
import requests | |
log = logging.getLogger('send-image') | |
HOST = 'ipad2-chris' | |
PORT = 8080 | |
def upload(host, port, path, show=True): | |
ext = os.path.splitext(path)[1][1:] | |
try: | |
with open(path, 'rb') as fp: | |
res = requests.put("http://%s:%s/assets/" % (host, port), | |
data=fp.read(), params=dict(type=ext)) | |
res = res.json() | |
if res.get('status') != 'OK': | |
raise IOError(res.get('msg', 'unknown error')) | |
except Exception as exc: | |
log.error("Upload failed: %s", exc) | |
else: | |
log.info("Image asset '%s' added.", res['id']) | |
if show: | |
log.debug("Requesting display of asset '%s'.", res['id']) | |
requests.get("http://%s:%s/assets/%s" % (host, port, res['id'])) | |
def main(args=None): | |
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | |
ap.add_argument('-H', '--host', metavar="HOST", default=HOST, | |
help="Host name of image frame device (default: %(default)s)") | |
ap.add_argument('-n', '--dont-show', action="store_true", | |
help="Don't show image after upload") | |
ap.add_argument('-p', '--port', metavar="PORT", default=PORT, type=int, | |
help="Port of image frame device server (default: %(default)s)") | |
ap.add_argument('-v', '--verbose', action="store_true", | |
help="Be verbose") | |
ap.add_argument('image', help="Path of image to upload") | |
args = ap.parse_args(args if args is not None else sys.argv[1:]) | |
logging.basicConfig(format="%(name)s: %(levelname)s - %(message)s", | |
level=logging.DEBUG if args.verbose else logging.WARNING) | |
upload(args.host, args.port, args.image, show=not args.dont_show) | |
if __name__ == '__main__': | |
sys.exit(main(sys.argv[1:]) or 0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment