diff --git a/README.md b/README.md index 04011b5a..72c22b85 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,49 @@ # photobooth - +Initiator: [![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/reuterbal) +Forker: +[![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](http://buymeacoff.ee/oelegeirnaert) + +I'm thirsty as well! + +## Edits by Oele Geirnaert: + +### Webinterface features +* When a new picture has been taken, it's automatically added to the Gallery & Slideshow via an API. + * A notification will popup in the gallery/slideshow that a new picture has been taken to motivate the people to take a selfie. +* Mail functionality via Mailgun +* Delete picture +* Generate QR code with links: + * QR code to show link + * QR code to mail the picture + * QR code to download the picture +* Keyboard shortcuts: + * **'g'**: Go to gallery + * **'s'**: Start slideshow + * **'esc'**: Go to index +* Added rand() function to safely store the picture on the webserver +* Web Views: + * Gallery-mode: This view is for administration purposes to view, mail, download, show qr codes to download/mail, print, delete a picture + * Slideshow-mode: This view may be used to show all the pictures on a projector in a room. + * Show only last picture mode: Last taken picture is shown + two QR codes to download/mail the picture) - This view may be set at the outside of the photobooth. + +- [ ] Security (That not everybody is able to get the gallery via the webserver) +- [ ] NFC +- [ ] Photobooth status (Free or Available with movement detector & GPIO) +- [ ] Other Improvements +- [ ] As this program becomes bigger and complexer, unit- & functional tests should be written. +- [ ] Inverse the default startup params (--gui must be set right now in orde to show the gui. -w activates the webserver. The way I run it the most: **python -m photobooth -w --gui** and while developing the webserver: **python -m photobooth --debug -w**) + + + +### Screenshots +Example of gallery +Example of slideshow +Example of mailform +Example of random picture names +Example of random picture names + A flexible Photobooth software. It supports many different camera models, the appearance can be adapted to your likings, and it runs on many different hardware setups. @@ -34,7 +76,7 @@ Screenshots produced using `CameraDummy` that produces unicolor images. * Based on [Python 3](https://www.python.org/), [Pillow](https://pillow.readthedocs.io), and [Qt5](https://www.qt.io/developers/) ### History -I started this project for my own wedding in 2015. +I started this project for my own wedding in 2015. See [Version 0.1](https://github.com/reuterbal/photobooth/tree/v0.1) for the original version. Github user [hackerb9](https://github.com/hackerb9/photobooth) forked this version and added a print functionality. However, I was not happy with the original software design and the limited options provided by the previously used [pygame](https://www.pygame.org) GUI library and thus abandoned the original version. diff --git a/photobooth/main.py b/photobooth/main.py index 63ddd7b5..9bbc1263 100644 --- a/photobooth/main.py +++ b/photobooth/main.py @@ -37,6 +37,7 @@ from .StateMachine import Context, ErrorEvent from .Threading import Communicator, Workers from .worker import Worker +from .webserver import Webserver # Globally install gettext for I18N gettext.install('photobooth', 'photobooth/locale') @@ -81,11 +82,16 @@ def __init__(self, argv, config, comm): self._comm = comm def run(self): + retval = None + parsed_args, unparsed_args = parseArgs(self._argv) + if parsed_args.gui: + logging.debug('Start GuiProcess') + Gui = lookup_and_import(gui.modules, self._cfg.get('Gui', 'module'), + 'gui') + retval = Gui(self._argv, self._cfg, self._comm).run() + else: + logging.debug("Gui process disabled.") - logging.debug('Start GuiProcess') - Gui = lookup_and_import(gui.modules, self._cfg.get('Gui', 'module'), - 'gui') - retval = Gui(self._argv, self._cfg, self._comm).run() logging.debug('Exit GuiProcess') return retval @@ -137,6 +143,28 @@ def run(self): logging.debug('Exit GpioProcess') +class WebServerProcess(mp.Process): + def __init__(self, argv, config, comm): + logging.debug(argv) + logging.debug(config) + logging.debug(comm) + + super().__init__() + self._argv = argv + self.daemon = True + self._cfg = config + self._comm = comm + + def run(self): + logging.debug("Start Webserver") + parsed_args, unparsed_args = parseArgs(self._argv) + + if parsed_args.webserver: + logging.debug("Run webserver") + ws = Webserver(self._cfg, self._comm).run() + else: + logging.debug("Webserver disabled") + def parseArgs(argv): @@ -146,6 +174,8 @@ def parseArgs(argv): help='omit welcome screen and run photobooth') parser.add_argument('--debug', action='store_true', help='enable additional debug output') + parser.add_argument('--webserver', '-w', action='store_true', help='start the webserver') + parser.add_argument('--gui', '-g', action='store_true', help='start gui') return parser.parse_known_args() @@ -165,7 +195,9 @@ def run(argv, is_run): # 3. GUI # 4. Postprocessing worker # 5. GPIO handler - proc_classes = (CameraProcess, WorkerProcess, GuiProcess, GpioProcess) + + proc_classes = (CameraProcess, WorkerProcess, GuiProcess, GpioProcess, WebServerProcess) + #proc_classes = (CameraProcess, WorkerProcess, WebServerProcess) procs = [P(argv, config, comm) for P in proc_classes] for proc in procs: diff --git a/photobooth/nfc/__init__.py b/photobooth/nfc/__init__.py new file mode 100644 index 00000000..396c8533 --- /dev/null +++ b/photobooth/nfc/__init__.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2013 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- + +import logging +log = logging.getLogger('main') + +import io +import time +import random +import argparse +import threading +import mimetypes + +from cli import CommandLineInterface + +import nfc +import nfc.snep +import nfc.ndef + +def add_send_parser(parser): + subparsers = parser.add_subparsers(title="send item", dest="send", + description="Construct beam data from the send item and transmit to " + "the peer device when touched. Use 'beam.py send {item} -h' to learn " + "additional and/or required arguments per send item.") + add_send_link_parser(subparsers.add_parser( + "link", help="send hyperlink", + description="Construct an NDEF Smartposter message with URI and the " + "optional title argument and send it to the peer when connected.")) + add_send_text_parser(subparsers.add_parser( + "text", help="send plain text", + description="Construct an NDEF Text message with the input string as " + "text content. The language default is 'en' (English) but may be set " + "differently with the --lang option.")) + add_send_file_parser(subparsers.add_parser( + "file", help="send disk file", + description="Embed the file content into an NDEF message and send " + "it to the peer when connected. The message type is guessed from " + "the file type using the mimetype module and the message name is " + "set to the file name unless explicitly set with command line flags " + "-t TYPE and -n NAME, respectively.")) + add_send_ndef_parser(subparsers.add_parser( + "ndef", help="send ndef data", + description="Send an NDEF message from FILE. If the file contains " + "multiple messages the strategy that determines the message to be " + "send can be set with the --select argument. For strategies that " + "select a different message per touch beam.py must be called with " + "the --loop flag. The strategies 'first', 'last' and 'random' select " + "the first, last or a random message from the file. The strategies " + "'next' and 'cycle' start with the first message and then count up, " + "the difference is that 'next' stops at the last message while " + "'cycle' continues with first.")) + parser.add_argument( + "--timeit", action="store_true", help="measure transfer time") + +def send_message(args, llc, message): + if args.timeit: + t0 = time.time() + if not nfc.snep.SnepClient(llc).put(message): + log.error("failed to send message") + elif args.timeit: + transfer_time = time.time() - t0 + message_size = len(str(message)) + print("message sent in {0:.3f} seconds ({1} byte @ {2:.0f} byte/sec)" + .format(transfer_time, message_size, message_size/transfer_time)) + +def add_send_link_parser(parser): + parser.set_defaults(func=run_send_link_action) + parser.add_argument( + "uri", help="smartposter uri") + parser.add_argument( + "title", help="smartposter title", nargs="?") + +def run_send_link_action(args, llc): + sp = nfc.ndef.SmartPosterRecord(args.uri) + if args.title: + sp.title = args.title + send_message(args, llc, nfc.ndef.Message(sp)) + +def add_send_text_parser(parser): + parser.set_defaults(func=run_send_text_action) + parser.add_argument( + "--lang", help="text language") + parser.add_argument( + "text", metavar="STRING", help="text string") + +def run_send_text_action(args, llc): + record = nfc.ndef.TextRecord(args.text) + if args.lang: + record.language = args.lang + send_message(args, llc, nfc.ndef.Message(record)) + +def add_send_file_parser(parser): + parser.set_defaults(func=run_send_file_action) + parser.add_argument( + "file", type=argparse.FileType('rb'), metavar="FILE", + help="file to send") + parser.add_argument( + "-t", metavar="TYPE", dest="type", default="unknown", + help="record type (default: mimetype)") + parser.add_argument( + "-n", metavar="NAME", dest="name", default=None, + help="record name (default: pathname)") + +def run_send_file_action(args, llc): + if args.type == 'unknown': + mimetype = mimetypes.guess_type(args.file.name, strict=False)[0] + if mimetype is not None: args.type = mimetype + if args.name is None: + args.name = args.file.name if args.file.name != "" else "" + + data = args.file.read() + try: data = data.decode("hex") + except TypeError: pass + + record = nfc.ndef.Record(args.type, args.name, data) + send_message(args, llc, nfc.ndef.Message(record)) + +def add_send_ndef_parser(parser): + parser.set_defaults(func=run_send_ndef_action) + parser.add_argument( + "ndef", metavar="FILE", type=argparse.FileType('rb'), + help="NDEF message file") + parser.add_argument( + "--select", metavar="STRATEGY", + choices=['first', 'last', 'next', 'cycle', 'random'], default="first", + help="strategies are: %(choices)s") + +def run_send_ndef_action(args, llc): + if type(args.ndef) == file: + bytestream = io.BytesIO(args.ndef.read()) + args.ndef = list() + while True: + try: args.ndef.append(nfc.ndef.Message(bytestream)) + except nfc.ndef.LengthError: break + args.selected = -1 + + if args.select == "first": + args.selected = 0 + elif args.select == "last": + args.selected = len(args.ndef) - 1 + elif args.select == "next": + args.selected = args.selected + 1 + elif args.select == "cycle": + args.selected = (args.selected + 1) % len(args.ndef) + elif args.select == "random": + args.selected = random.choice(range(len(args.ndef))) + + if args.selected < len(args.ndef): + send_message(args, llc, args.ndef[args.selected]) + +def add_recv_parser(parser): + subparsers = parser.add_subparsers(title="receive action", dest="recv", + description="On receipt of incoming beam data perform the specified " + "action. Use 'beam.py recv {action} -h' to learn additional and/or " + "required arguments per action.") + add_recv_print_parser(subparsers.add_parser( + "print", help="print received message", + description="Print the received NDEF message and do nothing else.")) + add_recv_save_parser(subparsers.add_parser( + "save", help="save ndef data to a disk file", + description="Save incoming beam data to a file. New data is appended " + "if the file does already exist, a parser can use the NDEF message " + "begin and end flags to separate messages.")) + add_recv_echo_parser(subparsers.add_parser( + "echo", help="send ndef data back to peer device", + description="Receive an NDEF message and send it back to the peer " + "device without any modification.")) + add_recv_send_parser(subparsers.add_parser( + "send", help="receive data and send an answer", + description="Receive an NDEF message and use the translations file " + "to find a matching response to send to the peer device. Each " + "translation is a pair of in and out NDEF message cat together.")) + +def add_recv_print_parser(parser): + parser.set_defaults(func=run_recv_print_action) + +def run_recv_print_action(args, llc, rcvd_ndef_msg): + log.info('print ndef message {0!r}'.format(rcvd_ndef_msg.type)) + print rcvd_ndef_msg.pretty() + +def add_recv_save_parser(parser): + parser.set_defaults(func=run_recv_save_action) + parser.add_argument( + "file", type=argparse.FileType('a+b'), + help="write ndef to file ('-' write to stdout)") + +def run_recv_save_action(args, llc, rcvd_ndef_msg): + log.info('save ndef message {0!r}'.format(rcvd_ndef_msg.type)) + args.file.write(str(rcvd_ndef_msg)) + +def add_recv_echo_parser(parser): + parser.set_defaults(func=run_recv_echo_action) + +def run_recv_echo_action(args, llc, rcvd_ndef_msg): + log.info('echo ndef message {0!r}'.format(rcvd_ndef_msg.type)) + nfc.snep.SnepClient(llc).put(rcvd_ndef_msg) + +def add_recv_send_parser(parser): + parser.set_defaults(func=run_recv_send_action) + parser.add_argument( + "translations", type=argparse.FileType('r'), + help="echo translations file") + +def run_recv_send_action(args, llc, rcvd_ndef_msg): + log.info('translate ndef message {0!r}'.format(rcvd_ndef_msg.type)) + if type(args.translations) == file: + bytestream = io.BytesIO(args.translations.read()) + args.translations = list() + while True: + try: + msg_recv = nfc.ndef.Message(bytestream) + msg_send = nfc.ndef.Message(bytestream) + args.translations.append((msg_recv, msg_send)) + log.info('added translation {0!r} => {1:!r}'.format( + msg_recv, msg_send)) + except nfc.ndef.LengthError: + break + for msg_recv, msg_send in args.translations: + if msg_recv == rcvd_ndef_msg: + log.info('rcvd beam {0!r}'.format(msg_recv)) + log.info('send beam {0!r}'.format(msg_send)) + nfc.snep.SnepClient(llc).put(msg_send) + break + +class DefaultServer(nfc.snep.SnepServer): + def __init__(self, args, llc): + self.args, self.llc = args, llc + super(DefaultServer, self).__init__(llc) + + def put(self, ndef_message): + log.info("default snep server got put request") + if self.args.action == "recv": + self.args.func(self.args, self.llc, ndef_message) + return nfc.snep.Success + +class Main(CommandLineInterface): + def __init__(self): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers( + title="actions", dest="action") + add_send_parser(subparsers.add_parser( + 'send', help='send data to beam receiver')) + add_recv_parser(subparsers.add_parser( + 'recv', help='receive data from beam sender')) + super(Main, self).__init__( + parser, groups="llcp dbg clf") + + def on_llcp_startup(self, llc): + self.default_snep_server = DefaultServer(self.options, llc) + return llc + + def on_llcp_connect(self, llc): + self.default_snep_server.start() + if self.options.action == "send": + func, args = self.options.func, ((self.options, llc)) + threading.Thread(target=func, args=args).start() + return True + +if __name__ == '__main__': + Main().run() diff --git a/photobooth/webserver/__init__.py b/photobooth/webserver/__init__.py new file mode 100644 index 00000000..4d70b0ea --- /dev/null +++ b/photobooth/webserver/__init__.py @@ -0,0 +1,296 @@ +#Thanks to: https://github.com/keijack/python-simple-http-server +import logging +import os, fnmatch +from simple_http_server import request_map +from simple_http_server import Response +from simple_http_server import MultipartFile +from simple_http_server import Parameter +from simple_http_server import Parameters +from simple_http_server import Header +from simple_http_server import JSONBody +from simple_http_server import HttpError +from simple_http_server import StaticFile +from simple_http_server import Headers +from simple_http_server import Cookies +from simple_http_server import Cookie +from simple_http_server import Redirect +from simple_http_server import PathValue + +from pathlib import Path +from time import localtime, strftime, time +import datetime +import requests +import configparser +import qrcode +import socket + +import simple_http_server.server as server + +class Webserver(object): + """docstring for Webserver.""" + + root = os.path.dirname(os.path.abspath(__file__)) + picture_dir = None + html_header = None + html_footer = None + mailgun_api_key = None + ip_address = None + webserver_hostname = None + webserver_port = None + printer = None + + def get_html_stream(str_filename): + my_html_file = "%s/templates/%s.html" %(Webserver.root, str_filename) + return open(my_html_file, 'r').read() + + def get_picture_meta_information(picture): + current_file = "%s/%s" %(Webserver.picture_dir,picture) + stat = os.stat(current_file) + current_picture_timestamp = None + try: + current_picture_timestamp = stat.st_birthtime + except AttributeError: + # We're probably on Linux. No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + current_picture_timestamp = stat.st_mtime + logging.debug(current_picture_timestamp) + current_picture_datetime = datetime.datetime.fromtimestamp(current_picture_timestamp) + current_picture_information = { + "picture_name": picture, + "picture_timestamp": str(current_picture_timestamp), + "picture_datetime": str(current_picture_datetime) + } + + return current_picture_information + + def get_full_html_stream(str_filename): + my_stream = Webserver.html_header + my_stream += Webserver.get_html_stream(str_filename) + my_stream += Webserver.html_footer + return my_stream + + def send_simple_message(name, email, message, filename, attachment): + logging.debug("Send picture %s to %s with message \n %s" %(attachment, email, message)) + return requests.post( + "https://api.mailgun.net/v3/oelegeirnaert.be/messages", + auth=("api", Webserver.mailgun_api_key), + files=[("inline", open(attachment,"rb"))], + data={"from": ["Oele Geirnaert's Photobooth","mailer@oelegeirnaert.be"], + "to": [name, email], + "subject": "Picture sent from Oele Geirnaert's Photobooth", + "text": message, + "html": 'Hello ' + name + ',

'+ message + '

Here is your picture:

' + }) + + def get_host_name(): + hostname = socket.gethostname() + return hostname + + def get_ip_address(): + hostname = socket.gethostname() + IPAddr = socket.gethostbyname(hostname) + return IPAddr + + + #As we may have only one server we can declare those variables globally + def __init__(self, config, comm): + super().__init__() + self._config = config + self._comm = comm + + path = config.get('Storage', 'basedir') + Webserver.picture_dir = strftime(path, localtime()) + Webserver.html_header = Webserver.get_html_stream("header") + Webserver.html_footer = Webserver.get_html_stream("footer") + + webserver_config = configparser.ConfigParser() + webserver_config_filename = os.path.join(os.path.dirname(__file__), 'config.cfg') + logging.debug("Reading config file from: %s" %webserver_config_filename) + my_config = webserver_config.read(webserver_config_filename) + Webserver.ip_address = webserver_config.get("Webserver", "ip_address") + Webserver.webserver_hostname = Webserver.get_host_name() + Webserver.mailgun_api_key = webserver_config.get("Mailgun", "api_secret") + Webserver.webserver_port = webserver_config.get("Webserver", "port") + Webserver.printer = webserver_config.get("Webserver", "printer") + logging.debug(Webserver.mailgun_api_key) + logging.debug(webserver_config.sections()) + + + @request_map("/favicon.ico") + def _favicon(): + return StaticFile("%s/favicon.ico" % Webserver.root, "image/x-icon") + + @request_map("/") + def index(): + logging.debug("Webserver root is: %s" % Webserver.root) + return Webserver.get_full_html_stream("index") + + @request_map("last") + def return_last_picture(): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("last_picture") + + @request_map("show_qrs") + def return_qrs(): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("show_qrs") + + @request_map("slideshow") + def start_slideshow(): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("slideshow") + + @request_map("picture/{picture}") + def return_single_picture(picture=PathValue()): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("single_picture") + + @request_map("settings") + def start_slideshow(): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("settings") + + @request_map("gallery") + def start_slideshow(): + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("gallery") + + @request_map("css/{css_file}") + def get_css_file(css_file=PathValue()): + my_css_file = "%s/css/%s" %(Webserver.root, css_file) + #my_css_stream = open(my_css_file, 'r').read() + return StaticFile(my_css_file, "text/css") + + @request_map("js/{js_file}") + def get_js_file(js_file=PathValue()): + my_js_file = "%s/js/%s" %(Webserver.root, js_file) + return StaticFile(my_js_file, "text/javascript") + + @request_map("send/picture/{picture}", method="POST") + def mail_picture(txt_email, txt_name, txt_message, picture=PathValue()): + logging.debug("Post method mail picture to %s" %txt_name) + my_pictures_path = "%s" %(Webserver.picture_dir) + attachment = "%s/%s" % (my_pictures_path, picture) + Webserver.send_simple_message(txt_name, txt_email, txt_message, picture, attachment) + return Redirect("/") + + #method=["GET", "POST"] + @request_map("/f/{function}/picture/{picture}") + def return_picture(picture=PathValue(), function=PathValue()): + logging.debug(function) + my_pictures_path = "%s" %(Webserver.picture_dir) + my_picture = "%s/%s" % (my_pictures_path, picture) + if function == 'download': + logging.debug("Download picture from: %s" %my_picture) + return StaticFile(my_picture, "application/octet-stream") + if function == 'show': + logging.debug("Show picture from: %s" %my_picture) + return StaticFile(my_picture, "image/jpg") + if function == 'delete': + logging.debug("Delete picture from: %s" %my_picture) + os.remove(my_picture) + return Redirect("/") + if function == 'single': + Webserver.html_header = Webserver.get_html_stream("header") + return Webserver.get_full_html_stream("single_picture") + if function == 'show_qr': + qr_code_img_path = Webserver.generate_qr('show', picture) + return StaticFile(qr_code_img_path, "image/jpg") + if function == 'download_qr': + qr_code_img_path = Webserver.generate_qr('download', picture) + return StaticFile(qr_code_img_path, "image/jpg") + if function == 'mail_qr': + qr_code_img_path = Webserver.generate_qr('mail', picture) + return StaticFile(qr_code_img_path, "image/jpg") + if function == 'mail': + logging.debug("Mail picture from: %s" %my_picture) + my_return_stream = Webserver.get_full_html_stream("frm_send_mail") + my_return_stream = my_return_stream.replace("[picture_id]", picture) + return my_return_stream + if function == 'print': + logging.debug("Print file %s ON: %s" %(my_picture, Webserver.printer)) + os.system("lpr -P %s %s" %(Webserver.printer, my_picture)) + else: + return Redirect("/") + + + def generate_qr(function, picture): + link = "http://%s:%s/f/%s/picture/%s" %(Webserver.ip_address, Webserver.webserver_port,function, picture) + img = qrcode.make(link) + qr_code_path = "%s/%s/%s_%s.png" %(Webserver.root, 'qrcodes', function, picture) + img.save(qr_code_path) + return qr_code_path + + + # @request_map("pictures/{picture}") + # def return_picture(picture=PathValue()): + # my_pictures_path = "%s" %(Webserver.picture_dir) + # my_picture = "%s/%s" % (my_pictures_path, picture) + # logging.debug("Loading picture from: %s" %my_picture) + # return StaticFile(my_picture, "image/jpg") + + @request_map("api/get_picture/{picture}") + def return_picture(picture=PathValue()): + logging.debug("requesting information for picture %s" %picture) + picture = "%s/%s" %(Webserver.picture_dir, picture) + logging.debug(picture) + picture_info = Webserver.get_picture_meta_information(picture) + return picture_info + + @request_map("api/get_new_pictures/{last_picture_timestamp}") + def return_new_pictures(last_picture_timestamp=PathValue()): + logging.debug("URL Timeparam: %s" %last_picture_timestamp) + image_list = [] + new_pictures = [] + all_pictures = [] + + my_last_picture_datetime= None + + if last_picture_timestamp.lower() == "undefined": + return {} + if last_picture_timestamp.upper() != "ALL": + try: + my_last_picture_datetime = datetime.datetime.fromtimestamp(float(last_picture_timestamp)) + except: + my_last_picture_datetime = datetime.datetime.now() + logging.warn("No valid timestamp") + + + image_list=fnmatch.filter(os.listdir(Webserver.picture_dir), '*.jpg') + current_picture_timestamp = None + current_picture_information = {} + for i in image_list: + current_picture_information = Webserver.get_picture_meta_information(i) + current_picture_timestamp = float(current_picture_information['picture_timestamp']) + if my_last_picture_datetime is not None: + print("current picture timestamp %s vs last picture timestamp %s" %(current_picture_timestamp, last_picture_timestamp)) + logging.debug("last picture timestamp: %s" %last_picture_timestamp) + if current_picture_timestamp > float(last_picture_timestamp) and last_picture_timestamp.upper() != "ALL": + logging.debug("Send picture") + new_pictures.append(current_picture_information) + all_pictures.append(current_picture_information) + + last_picture_information = max(all_pictures, key=lambda x:x['picture_timestamp']) + + return_pictures = [] + if last_picture_timestamp.upper() == "ALL": + return_pictures = all_pictures + else: + return_pictures = new_pictures + + return_pictures = sorted(return_pictures, key=lambda k: k['picture_timestamp']) + + thisdict = { + "time_param": str(last_picture_timestamp), + "last_picture": last_picture_information, + "new_pictures": return_pictures, + "now_datetime": str(datetime.datetime.now()), + "now_timestamp": str(time()), + "number_of_pictures": len(return_pictures), + "photobooth_status": "free" + } + return thisdict + + def run(self): + server.start() + return True diff --git a/photobooth/webserver/config_example.cfg b/photobooth/webserver/config_example.cfg new file mode 100644 index 00000000..a349c11a --- /dev/null +++ b/photobooth/webserver/config_example.cfg @@ -0,0 +1,7 @@ +[Mailgun] +api_secret = your-api-key + +[Webserver] +port = 9090 +ip_address = 192.168.0.194 +printer = "Samsung M2020" diff --git a/photobooth/webserver/css/gallery.css b/photobooth/webserver/css/gallery.css new file mode 100644 index 00000000..aa41683b --- /dev/null +++ b/photobooth/webserver/css/gallery.css @@ -0,0 +1,57 @@ +.gallery #new_pictures_added{ + position: fixed; + bottom: 0px; + right: 0px; + margin: 10px; + background-color: #cecece; + color: black; + padding: 10px; + z-index: 2; + border-radius: 4px; + border: 1px solid black; + box-shadow: 2px 5px 20px black; + text-align: center; +} + +.gallery .col{ + border-radius: 4px; + padding: 5px; + border: 1px solid black; + margin: 15px; + background-color: #cecece; + box-shadow: 2px 5px 20px black; +} + +.gallery .col img{ + background-color: black; +} + +.gallery .datetime, .gallery .picture_number{ + text-align: center; + background-color: black; + color: white; + padding: 5px 0px; +} + +.gallery .datetime{ + font-weight: bold; +} + +.gallery .action_links { + list-style: none; + padding-left: 0px; + width: 100%; + text-align: center; + margin: 15px 0px 10px; +} + +.gallery .action_links li{ + display: inline-block; + padding-right: 20px; +} + +.gallery .action_links li .fas { + color: black; + font-size: 1.3em; + text-shadow: 1px 1px white; +} diff --git a/photobooth/webserver/css/last_picture.css b/photobooth/webserver/css/last_picture.css new file mode 100644 index 00000000..55b2b0ff --- /dev/null +++ b/photobooth/webserver/css/last_picture.css @@ -0,0 +1,60 @@ +body.last_picture { + background-size: auto 100vh; + background-position: center top; + background-repeat: no-repeat; + background-color: black; +} + +.pos-footer{ + position: fixed; + bottom: 20px; + display: block; + text-align: center; +} + +.pos-left{ + left: 20px; +} + +.pos-right{ + right: 20px; +} + +.pos-middle{ + width: 100%; + margin: auto; + left: 0px; +} + +.qr { + width: 100%; +} + +.qr-box { + background-color: white; + width: 25vw; + border-radius: 4px; + border: 2px solid black; +} + +.qr-box{ + font-weight: bold; +} + +.event-title{ + text-align: center; + font-weight: bold; +} + +.picture-timestamp-box{ + background-color: black; + display: inline; + padding: 10px; + color: white; + z-index: -1; + margin-bottom: 20px; +} + +.timestamp{ + font-weight: bold; +} diff --git a/photobooth/webserver/css/shared.css b/photobooth/webserver/css/shared.css new file mode 100644 index 00000000..743c7b7f --- /dev/null +++ b/photobooth/webserver/css/shared.css @@ -0,0 +1,158 @@ +.box-shadow{ + box-shadow: 2px 5px 20px black; +} +.picture{ +width: 400px; +border: 2px solid black; +border-radius: 4px; +} + +div.picture_box { + vertical-align: top; + display: inline-block; + text-align: center; + margin: 10px 20px; +} + +img { + background-color: grey; +} + +.picture-btn{ +display: block; +margin-top: 10px; +font-weight: bold; +} + +.download { + +} + +.delete{ + +} + +.picture-container { + margin: auto auto; + width: 100% +} + +.mail_picture{ + width: 30%; + margin:auto; + text-align: center; +} + +/* SlideShow CSS */ + +#picture_list.slideshow img{ + width: 30%; +} + +.slideshow{ + cursor: none; +} + +/* Fading animation */ +.myFade { + -webkit-animation-name: myFade; + -webkit-animation-duration: 3s; + animation-name: myFade; + animation-duration: 3s; +} + +@-webkit-keyframes myFade { + from {opacity: .4} + to {opacity: 1} +} + +@keyframes myFade { + from {opacity: .4} + to {opacity: 1} +} + +/* Gallery */ +.gallery .mySlides { + display: block; +} + +/* Slideshow container */ +.slideshow-container { + max-width: 1000px; + position: relative; + margin: auto; +} + +.slideshow .numbertext{ + background-color: black; + font-size: 2em; + color: white; + width: 50px; + height: 50px; + display: block; + position: relative; + bottom:10px; + left:10px; + border-radius: 50%; + text-align: center; + padding-bottom: 5px; + border: 2px solid white; + z-index: 1; +} + +* {box-sizing: border-box;} +body {font-family: Verdana, sans-serif;} +.mySlides {display: none;} +img {vertical-align: middle;} + +/* Caption text */ +.slideshow .text { + color: #f2f2f2; + background-color: black; + font-size: 2em; + padding: 8px 12px; + position: absolute; + bottom: -56px; + width: 100%; + text-align: center; +} + +body.slideshow{ + background-color: black; + color: white; + /* Full height */ + height: 100vh; + + /* Center and scale the image nicely */ + background-position: center; + background-repeat: no-repeat; + /* background-size: cover; */ + background-size: auto 100%; + + transition: background .25s ease-in-out; + -moz-transition: background .25s ease-in-out; + -webkit-transition: background .25s ease-in-out; +} + +.slideshow h1{ + +} + +.slideshow #clock{ + +} + +.infoblock.middle .datetimestamp, #picture_number{ + background-color: black; + display: inline-block; + padding: 10px; + border-radius: 4px; +} + +.header.infoblock{ + top: 0px; +} + +body.slideshow, html.slideshow { + height: 100vh; +} diff --git a/photobooth/webserver/css/show_qrs.css b/photobooth/webserver/css/show_qrs.css new file mode 100644 index 00000000..e69de29b diff --git a/photobooth/webserver/css/slideshow.css b/photobooth/webserver/css/slideshow.css new file mode 100644 index 00000000..5998fc8a --- /dev/null +++ b/photobooth/webserver/css/slideshow.css @@ -0,0 +1,49 @@ +.slideshow #new_pictures_added{ + font-size: 3em; + text-align: center; + margin-top: 45vh; + display: block; + position: absolute; + width: 100vw; + background-color: white; + color: black; +} + +.slideshow #photobooth_status .free{ + color: green; +} + +.slideshow .infoblock{ + position: absolute; + display: block; + padding: 20px; + font-weight: bold; + width: 240px; + text-align: center; +} + +.slideshow .infoblock .big{ + font-size: 2em; + font-weight: normal; + text-transform: uppercase; +} + +.slideshow .infoblock.right{ + right: 0px; +} + +.slideshow .infoblock.middle{ + margin: 0px auto; + width: 100%; +} + +.slideshow #new_pictures_added p{ + font-size: 0.5em; + color: red; + font-weight: bold; +} + +.slideshow .footer{ + bottom: 0px; + font-size: 1.2em; +} diff --git a/photobooth/webserver/favicon.ico b/photobooth/webserver/favicon.ico new file mode 100644 index 00000000..a13c248e Binary files /dev/null and b/photobooth/webserver/favicon.ico differ diff --git a/photobooth/webserver/js/gallery.js b/photobooth/webserver/js/gallery.js new file mode 100644 index 00000000..0cc003b8 --- /dev/null +++ b/photobooth/webserver/js/gallery.js @@ -0,0 +1,5 @@ +console.log("I'm your gallery script!") + +view = "gallery"; + +// i, data, view, object diff --git a/photobooth/webserver/js/last_picture.js b/photobooth/webserver/js/last_picture.js new file mode 100644 index 00000000..328db34d --- /dev/null +++ b/photobooth/webserver/js/last_picture.js @@ -0,0 +1,14 @@ +console.log("Hello, i'm your last_picture.js file"); +window.setInterval(function(){ + console.log("kiekeboe"); + set_last_picture_info("#last_image", "#last_picture", last_image); +}, do_api_call_every_x_seconds * 1000); + +function set_last_picture_info(id, picture_container, data){ + // my_img = $(picture_container); + // my_img.attr("style","f/show/picture/"+data.picture_name); + $('body').css('background-image', 'url(/f/show/picture/' + data.picture_name + ')'); + $('#picture_timestamp p').text(format_datetime(data.picture_datetime, 'DATETIME')); + $('#mail_qr').attr('src', '/f/mail_qr/picture/' + data.picture_name); + $('#download_qr').attr('src','/f/download_qr/picture/' + data.picture_name); +} diff --git a/photobooth/webserver/js/settings.js b/photobooth/webserver/js/settings.js new file mode 100644 index 00000000..608abfda --- /dev/null +++ b/photobooth/webserver/js/settings.js @@ -0,0 +1,13 @@ +// probably it will take more than 60 seconds to take a picture in the booth. +do_api_call_every_x_seconds = 5; + +change_slides_every_x_secondes = 5; +lst_images = []; +last_image = ""; +show_in_fullscreen = true; +show_new_pictures_popup_for_x_seconds = 5; +time_locale = 'nl-BE'; +time_format = {hour: '2-digit', minute:'2-digit'} +datetime_format = {year: 'numeric', month:'long', day:'numeric', hour:'2-digit', minute:'2-digit'} +gallery_number_of_columns = 3; +gallery_column_width = 'col' diff --git a/photobooth/webserver/js/shared.js b/photobooth/webserver/js/shared.js new file mode 100644 index 00000000..2d3c6dfd --- /dev/null +++ b/photobooth/webserver/js/shared.js @@ -0,0 +1,181 @@ +console.log("Hello, i'm your shared javascript file."); + +// For keyboard interaction +$(document).keyup(function(e) { + console.log(e.keyCode); + if (e.keyCode === 27) window.location.href = "/"; // esc > Go to index + if (e.keyCode === 13 || e.keyCode === 83) window.location.href = "/slideshow"; // enter > Start slideshow + if (e.keyCode === 71) window.location.href = "/gallery" +}); + + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString +function format_datetime(datetimestamp, format){ + if(datetimestamp.toUpperCase() == "NOW"){ + d = new Date() + }else { + d = new Date(datetimestamp); + } + if(format.toUpperCase() == "TIME"){ + d = d.toLocaleTimeString(time_locale, time_format); + return d; + } + if(format.toUpperCase() == 'DATETIME'){ + d = d.toLocaleTimeString(time_locale, datetime_format); + return d; + } +} + +function update_clock() +{ + now_time = format_datetime('now', 'time'); + $("#clock").html("It's now:

" + now_time + "

"); + setTimeout(update_clock, 1000); +} + +t = 'all' +window.setInterval(function(){ + execute_api_call_get_new_pictures(t, view); +}, do_api_call_every_x_seconds * 1000); + +function execute_api_call_get_new_pictures(timestamp, view) +{ + url = "api/get_new_pictures/" + timestamp; + console.log(url); + var jqxhr = $.getJSON( url, function(data) { + number_of_new_pictures = data['number_of_pictures']; + if(number_of_new_pictures > 0) + { + show_new_pictures_popup(number_of_new_pictures) + } + append_new_pictures(data.new_pictures, view); + show_photoboot_status(data.photobooth_status); + last_image = data['last_picture'] + t= data['last_picture']['picture_timestamp']; + }) + .done(function(data) { + console.log("ready") + }) + .fail(function() { + console.log("error"); + }); +} + +function show_new_pictures_popup(number) +{ + if(number==1) + pictures = "picture was" + else { + pictures = "pictures were" + } + $("#new_pictures_added").html("Hooray, " + number + " new " + pictures + " taken...

Go and grab yours now!

").fadeIn().delay(show_new_pictures_popup_for_x_seconds * 1000).fadeOut(); +} + +function prepare_window(name, remove_container){ + $("", { + rel: "stylesheet", + type: "text/css", + href: "/css/" + name + ".css" + }).appendTo("head"); + + $(" diff --git a/photobooth/webserver/templates/header.html b/photobooth/webserver/templates/header.html new file mode 100644 index 00000000..d5ff37db --- /dev/null +++ b/photobooth/webserver/templates/header.html @@ -0,0 +1,16 @@ + + + + Photobooth + + + + + + + + + + + +
diff --git a/photobooth/webserver/templates/index.html b/photobooth/webserver/templates/index.html new file mode 100644 index 00000000..2ec92a6a --- /dev/null +++ b/photobooth/webserver/templates/index.html @@ -0,0 +1,7 @@ +

Hello, welcome

+Open gallery +Start slideshow +Open settings +Show last picture + + diff --git a/photobooth/webserver/templates/last_picture.html b/photobooth/webserver/templates/last_picture.html new file mode 100644 index 00000000..ca3a59ae --- /dev/null +++ b/photobooth/webserver/templates/last_picture.html @@ -0,0 +1,22 @@ +
ExampleEvent
+ + + + + + + diff --git a/photobooth/webserver/templates/settings.html b/photobooth/webserver/templates/settings.html new file mode 100644 index 00000000..fba77750 --- /dev/null +++ b/photobooth/webserver/templates/settings.html @@ -0,0 +1,4 @@ +

Settings

+ + + diff --git a/photobooth/webserver/templates/show_qrs.html b/photobooth/webserver/templates/show_qrs.html new file mode 100644 index 00000000..c45165bb --- /dev/null +++ b/photobooth/webserver/templates/show_qrs.html @@ -0,0 +1,6 @@ +test + + diff --git a/photobooth/webserver/templates/single_picture.html b/photobooth/webserver/templates/single_picture.html new file mode 100644 index 00000000..4b0ec176 --- /dev/null +++ b/photobooth/webserver/templates/single_picture.html @@ -0,0 +1,5 @@ +

Single picture

+
+ + + diff --git a/photobooth/webserver/templates/slideshow.html b/photobooth/webserver/templates/slideshow.html new file mode 100644 index 00000000..0762add3 --- /dev/null +++ b/photobooth/webserver/templates/slideshow.html @@ -0,0 +1,24 @@ +
+ +
+
Loading pictures
+ + +
+
+
+ + + +
+ +
+ + + diff --git a/photobooth/worker/PictureList.py b/photobooth/worker/PictureList.py index 73a3ed5f..60dede90 100644 --- a/photobooth/worker/PictureList.py +++ b/photobooth/worker/PictureList.py @@ -19,6 +19,7 @@ import logging import os +import random from glob import glob diff --git a/photobooth/worker/__init__.py b/photobooth/worker/__init__.py index fc36f95f..e6505812 100644 --- a/photobooth/worker/__init__.py +++ b/photobooth/worker/__init__.py @@ -19,6 +19,7 @@ import logging import os.path +import random, string from time import localtime, strftime @@ -49,7 +50,16 @@ def __init__(self, basename): def do(self, picture): filename = self._pic_list.getNext() + logging.info('Saving picture as %s', filename) + + + random_string = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(6)) + if "rand()" in filename: + logging.info("Changing name with random string: %s..." %random_string) + filename = filename.replace('rand()', random_string) + + with open(filename, 'wb') as f: f.write(picture.getbuffer()) diff --git a/screenshots/photobooth_gallery.png b/screenshots/photobooth_gallery.png new file mode 100644 index 00000000..988c99ef Binary files /dev/null and b/screenshots/photobooth_gallery.png differ diff --git a/screenshots/photobooth_last_picture.png b/screenshots/photobooth_last_picture.png new file mode 100644 index 00000000..02d50131 Binary files /dev/null and b/screenshots/photobooth_last_picture.png differ diff --git a/screenshots/photobooth_mail_form.png b/screenshots/photobooth_mail_form.png new file mode 100644 index 00000000..dfa7db16 Binary files /dev/null and b/screenshots/photobooth_mail_form.png differ diff --git a/screenshots/photobooth_rand_picture.png b/screenshots/photobooth_rand_picture.png new file mode 100644 index 00000000..7c388828 Binary files /dev/null and b/screenshots/photobooth_rand_picture.png differ diff --git a/screenshots/photobooth_slideshow.png b/screenshots/photobooth_slideshow.png new file mode 100644 index 00000000..cc84fafa Binary files /dev/null and b/screenshots/photobooth_slideshow.png differ