diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7da1f96 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..16a9db2 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,22 @@ +name: Code Quality + +on: + push: + paths: + - "**.py" + +jobs: + lint: + name: Python Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: "3.8" + - name: Run flake8 + uses: julianwachholz/flake8-action@v1 + with: + checkName: "Python Lint" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 49fbd99..785f5e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ -*.log settings.yml +*.avi +*.pyc +*.db +*.sublime* +*.log +oldversions/* +*.bak +static/images/* +static/videos/* diff --git a/networking.py b/networking.py new file mode 100644 index 0000000..95466ba --- /dev/null +++ b/networking.py @@ -0,0 +1,179 @@ +import struct +import os +import socket +import logging +import pickle + +import settings +import services + + +# SENDING AND RECEIVING +def send_msg(sock, msg): + """ Prefix each message with a 4-byte length (network byte order) """ + msg = struct.pack('>I', len(msg)) + msg + sock.sendall(msg) + + +def recv_msg(sock): + """ Read message length and unpack it into an integer """ + raw_msglen = recvall(sock, 4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + + +def recvall(sock, n): + """ Helper function to recv n bytes or return None if EOF is hit """ + data = '' + + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + print(packet) + data += packet + + return data + + +class Connection: + def __init__(self): + SETTINGS = services.read_settings(settings_file="settings.json") + print(SETTINGS) + if SETTINGS: + self.PROJECTRS = SETTINGS["projectors"] + else: + self.PROJECTRS = [] + self.connections = {} + + @staticmethod + def get_display_addr(display): + return (display["ip"], display["port"]) + + def init_network(self): + logging.info("Check display connections") + for display in self.PROJECTRS: + if not self.PROJECTRS[display]["enabled"]: + continue + + if display not in self.connections: + logging.info("Display not in existing connections") + self.connect_display(display) + else: + self.send_msg_to_display(display, "alive") + try: + # Try to receive from connection to see if it is alive + recv_msg(self.connections[display]) + logging.info("Already connected to %s" % display) + except Exception: + logging.exception("Connection appears to be dead") + self.connect_display(display) + logging.info("Connections: %s" % self.connections) + + def connect_display(self, display): + """ Try and connect to a display + This is a bit of a clusterfuck + should probably re-engineer this """ + # Make a new socket + logging.info("Connections: " % self.connections) + try: + logging.info("Remove display socket from connections") + del self.connections[display] + except KeyError: + logging.info("Display doesn't exist in connections") + + # Make new socket it and add to connections dict + self.connections[display] = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + self.connections[display].settimeout(1) + + try: + # try connecting to socket + self.connections[display].connect( + self.get_display_addr(self.PROJECTRS[display])) + logging.info("Connected to %s" % display) + # set socket timeout high + self.connections[display].settimeout(20) + return True + except: + logging.info("No displays found") + # delete socket from dict, because it doesn't work + del self.connections[display] + # increment attempts + return False + + def send_msg_to_display(self, display, msg): + """ Send a message to a display """ + logging.info("Send message to display") + msg = pickle.dumps(msg) + logging.info(msg) + sent = False + attempts = 1 + while not sent and attempts < 4: + try: + sock = self.connections[display] + sent = True + except KeyError: + logging.exception("Display %s doesn't exist" % display) + logging.info("Reattach display, attempt %d" % attempts) + display_connected = self.connect_display(display) + + if display_connected: + logging.info("Successfully reconnected display") + else: + logging.info("Could not reattach display") + attempts += 1 + + if display not in self.connections: + logging.info("Could not send message, could not communicate with display") + return + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + + # Try sending message, if broken pipe + # Try creating new socket + attempts = 0 + while attempts < 3: + try: + logging.info("Attempting to send message") + sock.sendall(msg) + logging.info("Sent message %s" % msg) + break + except socket.error as e: + logging.info("Socket error: %s" % e) + logging.info("Attempting to reconnect to display %s" % sock) + logging.info(self.connections) + connected = self.connect_display(display) + if not connected: + attempts += 1 + + def whatsplaying(self, display): + message = {"action": "whatsplaying"} + current_image = None + self.send_msg_to_display(display, message) + # Wait for reply + try: + current_image = recv_msg(self.connections[display]) + logging.info("Received TCP data: %s from %s" % + (current_image, self.connections[display])) + except socket.timeout: + logging.info("No reply, socket timed out") + except KeyError: + logging.info("Display not found") + return current_image + + def project(self, display, image): + message = {"action": "project", + "images": [os.path.join(settings.IMAGEDIR, image)]} + self.send_msg_to_display(display, message) + logging.info("Image to be projected is %s" % image) + self.PROJECTRS[display]["current"] = image + + def slideshow(self, display, imagelist): + message = {"action": "slideshow", "images": imagelist} + self.send_msg_to_display(display, message) diff --git a/orm.py b/orm.py new file mode 100644 index 0000000..db0ff30 --- /dev/null +++ b/orm.py @@ -0,0 +1,11 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +# CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); +class Image(db.Model): + Id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String()) + imagename = db.Column(db.String()) + folder = db.Column(db.String()) diff --git a/projector.py b/projector.py index 7601e00..422450a 100644 --- a/projector.py +++ b/projector.py @@ -1,5 +1,9 @@ #!/usr/bin/python +""" +Projectr - Projector Process +Works in tandem with Server Process +""" from __future__ import absolute_import, division, print_function, unicode_literals import pi3d @@ -7,135 +11,71 @@ import os import logging import time -import sys -from datetime import datetime import argparse import multiprocessing -import yaml # Networking -import struct -import thread +import threading import socket import pickle -""" -Projectr - Projector Process -Works in tandem with Server Process +import networking +import services -""" logging.basicConfig( filename="projector.log", - level=logging.INFO, format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', ) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) +client_logger = logging.getLogger('client_handler') +client_logger.setLevel(logging.DEBUG) +client_logger.addHandler(logging.StreamHandler()) +projectr_logger = logging.getLogger('projectr') +projectr_logger.setLevel(logging.DEBUG) +projectr_logger.addHandler(logging.StreamHandler()) IMAGEDIR = 'static/images/' - -""" FUNCTIONS """ - -# Use class for settings? -def write_settings(process, data): - """Write the previous image to settings file""" - logging.info("Writing settings...") - logging.info("Writing settings...") - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) - - -def read_settings(process): - logging.info("Read settings...") - try: - return yaml.load(open("settings.yml")) - except: - logging.exception("Could not read settings") - logging.info("Writing default settings file") - data = { - 'slideshow': {'delay': 20, 'loop': True}, - 'fadeduration': 2, - 'lastimage': u'static/images/logo.jpg', - 'projectors': { - 'local': { - 'ip': '127.0.0.1', - 'port': 5006, - 'enabled': True, - 'name': 'Main', - 'current': '', - } - } - } - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) - return data - - -def fit_image(input_texture): +ALPHA_STEP = 0.025 +# TCP config +TCP_IP = "127.0.0.1" # local only +TCP_PORT = 5006 + + +DEFAULT_SETTINGS = { + 'slideshow': {'delay': 20, 'loop': True}, + 'fadeduration': 2, + 'lastimage': u'static/images/logo.jpg', + 'projectors': { + 'local': { + 'ip': TCP_IP, + 'port': TCP_PORT, + 'enabled': True, + 'name': 'Main', + 'current': '', + } + } +} + + +def fit_image(input_texture, display): """ Fit image to screen """ # Ripped this from demo, think I understand it # Pretty sure this bit resizes textureture to display size # Get ratio of display to textureture - x_ratio = DISPLAY.width/input_texture.ix - y_ratio = DISPLAY.height/input_texture.iy + x_ratio = display.width / input_texture.ix + y_ratio = display.height / input_texture.iy if y_ratio < x_ratio: # if y ratio is smaller than x ratio x_ratio = y_ratio # make the ratios the same width, height = input_texture.ix * x_ratio, input_texture.iy * x_ratio # width, height = tex.ix, tex.iy - x_position = (DISPLAY.width - width)/2 - y_position = (DISPLAY.height - height)/2 + x_position = (display.width - width) / 2 + y_position = (display.height - height) / 2 return width, height, x_position, y_position -def list_files(directory, reverse=False): - """ Return list of files of specified type """ - output = [f for f in os.listdir(directory) if - os.path.isfile(os.path.join(directory, f)) and - f.endswith(('.jpg', '.jpeg', '.png'))] - - if reverse is True: - # Sort newFileList by date added(?) - output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) - output.reverse() # reverse image list so new files are first - else: - pass - - return output - - -""" NETWORKING """ - - -# SENDING AND RECEIVING -def send_msg(sock, msg): - """ Prefix each message with a 4-byte length (network byte order) """ - msg = struct.pack('>I', len(msg)) + msg - sock.sendall(msg) - - -def recv_msg(sock): - """ Read message length and unpack it into an integer """ - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - """ Helper function to recv n bytes or return None if EOF is hit """ - data = '' - - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: - return None - print(packet) - data += packet - - return data - - """ PROCESSES """ @@ -143,47 +83,48 @@ def slideshow(imagelist, cur_queue, killslideshowq): """ run slideshow """ process = "Slideshow Process" - settings = read_settings(process) - logging.info("Starting slideshow...") + settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) + client_logger.info("Starting slideshow...") working = True - while working is True: + while working: for image in imagelist: # Check to see if slideshow needs to die if not killslideshowq.empty(): emptyq = killslideshowq.get() if emptyq == "die": - logging.info("Killing slideshow") + client_logger.info("Killing slideshow") working = False # Make sure while loop breaks break # break out of for loop - logging.info("Slideshow: %s", image) + client_logger.info("Slideshow: %s", image) cur_queue.put(image) time.sleep(settings["slideshow"]["delay"] + settings["fadeduration"]) def client_handler(connection, client_address, cur_queue): process = "Client Handler - %s" % os.urandom(8).encode('base_64') - logging.debug(connections) # Is there a slideshow running? slideshowon = False killslideshowq = multiprocessing.Queue() - logging.info("Connected to Master") + client_logger.info("Connected to Master") while True: - # buffer size is 1024 bytes - data = recv_msg(connection) + data = networking.recv_msg(connection) - if data != "alive": + if data == "alive": + client_logger.info("Alive?") + networking.send_msg(connection, "alive") + else: try: data = pickle.loads(data) - logging.debug(data) - except TypeError, e: - logging.exception("Failed to unpickle data") + client_logger.debug(data) + except TypeError: + client_logger.exception("Failed to unpickle data") - logging.info("Received TCP data: %s from %s" % (data, client_address)) + client_logger.info("Received TCP data: %s from %s" % (data, client_address)) - if slideshowon is True: + if slideshowon: # If slideshow is running, kill it - logging.info("Tell Slideshow process to die") + client_logger.info("Tell Slideshow process to die") # Tell slideshow to die killslideshowq.put("die") slideshowon = False @@ -193,192 +134,150 @@ def client_handler(connection, client_address, cur_queue): cur_queue.put(data["images"][0]) slideshowon = False elif "video" in data: - logging.info("Video to project is %s", data["video"]) + client_logger.info("Video to project is %s", data["video"]) else: - logging.info("Nothing to project") + client_logger.info("Nothing to project") elif data["action"] == "slideshow": - logging.info("Start Slideshow Process") - ssproc = multiprocessing.Process(target=slideshow, - args=(data["images"], - cur_queue, - killslideshowq)) + client_logger.info("Start Slideshow Process") + ssproc = multiprocessing.Process( + target=slideshow, args=(data["images"], + cur_queue, + killslideshowq)) ssproc.start() slideshowon = True elif data["action"] == "stopslideshow": ssproc.terminate() elif data["action"] == "sync": - images = list_files(IMAGEDIR) + images = services.list_files(IMAGEDIR) print(images) elif data["action"] == "whatsplaying": - settings = read_settings(process) - send_msg(connection, settings["lastimage"]) + settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) + networking.send_msg(connection, settings["lastimage"]) else: - logging.info("Unkown action: %s", data["action"]) - else: - logging.info("Alive?") - send_msg(connection, "alive") + client_logger.info("Unkown action: %s", data["action"]) def tcp_receiver(cur_queue, connections): """ Receive images via TCP """ - process = "TCP Receiver" - - # TCP config - tcp_ip = "127.0.0.1" # local only - tcp_port = 5006 - # Set up TCP # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - logging.info('Starting up on %s port %s' % (tcp_ip, tcp_port)) - + client_logger.info('Starting up on %s port %s' % (TCP_IP, TCP_PORT)) # bind server to address - sock.bind((tcp_ip, tcp_port)) + sock.bind((TCP_IP, TCP_PORT)) sock.listen(1) cons = {} while True: - logging.info("Waiting for a connection...") + client_logger.info("Waiting for a connection...") # Not sure if this is really necessary however, # It gives the connection a unique number - # and adds it to the connactions dictionary + # and adds it to the connections dictionary con_number = str(len(connections) + 1) - logging.debug(con_number) + client_logger.debug(con_number) cons[con_number], client_address = sock.accept() - logging.info("Starting a new thread for client %s", str(client_address)) - thread.start_new_thread(client_handler, (cons[con_number], - client_address, cur_queue)) - - -""" pi3d """ + client_logger.info("Starting a new thread for client %s", str(client_address)) + threading.Thread( + target=client_handler, + args=(cons[con_number], client_address, cur_queue) + ) class Carousel(object): """ The main object """ - def __init__(self): - # Start the image dictionary + display_settings = { + "background": (0.0, 0.0, 0.0, 1.0), "frames_per_second": 20} + + def __init__(self, test=False): self.imagedict = {} self.process = "Carousel" - - # Load the last image used - try: - # If there is a settings file, load most recent image - # Load the settings file - - settings = read_settings(self.process) - # Add the image to the queue - logging.info("Last image: %s" % settings["lastimage"]) - starting_image = settings["lastimage"] - except: - logging.exception("Failed to load settings file") - logging.info("Loading default image") - # Write new image to settings - settings = read_settings(self.process) - settings["lastimage"] = "static/images/logo.jpg" - write_settings(self.process, settings) - settings = read_settings(self.process) - # Load default image into queue - starting_image = settings["lastimage"] - - # Set up image one - texture_one = pi3d.Texture(starting_image, blend=True, mipmap=True) - image_one = pi3d.Canvas() - image_one.set_texture(texture_one) - - width, height, x_position, y_position = fit_image(texture_one) - - image_one.set_2d_size(w=width, h=height, x=x_position, y=y_position) - image_one.set_alpha(1) - image_one.set_shader(SHADER) - image_one.positionZ(0.1) - - self.imagedict[starting_image] = {"canvas": image_one, "visible": True, - "fading": True} - + if test: + self.display = pi3d.Display.create( + w=800, h=600, **self.display_settings) + else: + self.display = pi3d.Display.create(**self.display_settings) + self.shader = pi3d.Shader("2d_flat") + starting_image = self.get_starting_image() + self.set_up_image(starting_image, 1, 0.1) self.focus = starting_image + # Set up camera + self.camera = pi3d.Camera.instance() + self.camera.was_moved = False + self.keyboard = pi3d.Keyboard() + self.image_queue = multiprocessing.Queue() - # print(self.imagedict[self.focus]["canvas"].z()) + def get_starting_image(self): + # Load the last image used + # If there is a settings file, load most recent image + settings = services.read_settings( + settings_file='settings.json', default_settings=DEFAULT_SETTINGS) + if not settings: + services.write_settings(DEFAULT_SETTINGS, settings_file='settings.json') + settings = services.read_settings( + settings_file='settings.json', default_settings=DEFAULT_SETTINGS) + projectr_logger.info("Last image: %s" % settings["lastimage"]) + return settings["lastimage"] + + def set_up_image(self, image, alpha, z_position): + texture = pi3d.Texture(image, blend=True, mipmap=True) + canvas = pi3d.Canvas() + canvas.set_texture(texture) + width, height, x_position, y_position = fit_image(texture, self.display) + canvas.set_2d_size(w=width, h=height, x=x_position, y=y_position) + canvas.set_alpha(alpha) + canvas.set_shader(self.shader) + canvas.positionZ(z_position) + self.imagedict[image] = { + "canvas": canvas, "visible": True, "fading": True} def pick(self, new_image): """ Pick an image by URL """ + if self.focus == new_image: + projectr_logger.warning("Image already projected") + return + + # Check to see if image already in dictionary + # Might be worth pre-preparing + # a dictionary with null canvas objects? + + # If image is already loaded, make it visible + if new_image in self.imagedict: + # New focus image is visible + # New focus image is the active fader + self.imagedict[new_image]["visible"] = True + self.imagedict[new_image]["fading"] = True + else: + self.set_up_image(new_image, 0, 0.1) - if self.focus != new_image: - # Check to see if image already in dictionary - # Might be worth pre-preparing - # a dictionary with null canvas objects? - - # If image is already loaded, make it visible - if new_image in self.imagedict: - # print("Image exists") - # New focus image is visible - self.imagedict[new_image]["visible"] = True - - # New focus image is the active fader - self.imagedict[new_image]["fading"] = True - - # Otherwise load it as a new image - else: - # print("Load new image") - - new_canvas = pi3d.Canvas() - - # print("Pick an image: %s" % new_image) - new_texture = pi3d.Texture(new_image, blend=True, mipmap=True) - # print("Texture loaded") - new_canvas.set_texture(new_texture) - # print("Texture set") - - # Fit image - width, height, x_position, y_position = fit_image(new_texture) - - new_canvas.set_2d_size(w=width, h=height, - x=x_position, y=y_position) - new_canvas.set_alpha(0) - new_canvas.set_shader(SHADER) - new_canvas.positionZ(0.2) - # print("New image prepared") - - self.imagedict[new_image] = {"canvas": new_canvas, - "visible": True, "fading": True} - - # Move old focused image back - - if self.imagedict[self.focus]["canvas"].z() > 0.1: - self.imagedict[self.focus]["canvas"].positionZ(0.1) + # Move old focused image back + if self.imagedict[self.focus]["canvas"].z() > 0.1: + self.imagedict[self.focus]["canvas"].positionZ(0.1) - # Bring new focused image forward - self.imagedict[new_image]["canvas"].positionZ(0.2) + # Bring new focused image forward + self.imagedict[new_image]["canvas"].positionZ(0.2) - # Old focus image not the active fader - self.imagedict[self.focus]["fading"] = False + # Old focus image not the active fader + self.imagedict[self.focus]["fading"] = False - self.focus = new_image # Change the focused image - # Write new image to settings - settings = read_settings(self.process) - settings["lastimage"] = new_image - write_settings(self.process, settings) - settings = read_settings(self.process) - else: - logging.warning("Image already projected") + self.focus = new_image # Change the focused image + # Write new image to settings + services.update_setting('lastimage', new_image, file_name='settings.json') def update(self): """ Update image alphas """ - for image in self.imagedict: - alpha = self.imagedict[image]["canvas"].alpha() - if self.imagedict[image]["fading"] is True and alpha < 1: - # print("%s Increase alpha: %f" % (image, alpha)) - alpha += alpha_step - self.imagedict[image]["canvas"].set_alpha(alpha) - elif self.imagedict[image]["fading"] is False and alpha > 0: - # print("%s Decrease alpha: %f" % (image, alpha)) - alpha -= alpha_step - self.imagedict[image]["canvas"].set_alpha(alpha) + for image, detail in self.imagedict.items(): + alpha = detail["canvas"].alpha() + if detail["fading"] and alpha < 1: + alpha += ALPHA_STEP + detail["canvas"].set_alpha(alpha) + elif not detail["fading"] and alpha > 0: + alpha -= ALPHA_STEP + detail["canvas"].set_alpha(alpha) else: if alpha <= 0: - self.imagedict[image]["visible"] = False + detail["visible"] = False def draw(self): """ Draw the images on the screen """ @@ -386,82 +285,59 @@ def draw(self): first_image = None second_image = None - for image in self.imagedict: - if self.imagedict[image]["visible"] is True and self.imagedict[image]["fading"] is True: - first_image = self.imagedict[image]["canvas"] - elif self.imagedict[image]["visible"] is True: - second_image = self.imagedict[image]["canvas"] + for image, detail in self.imagedict.items(): + if detail["visible"] and detail["fading"]: + first_image = detail["canvas"] + elif detail["visible"]: + second_image = detail["canvas"] first_image.draw() # If second image exists (fixes start up) if second_image: second_image.draw() + def loop(self, tcpprocess, connections): + projectr_logger.info("Start Projector process") + while self.display.loop_running(): + self.update() + self.draw() + + k = self.keyboard.read() + if k > -1: + if k == 27: + self.keyboard.close() + self.display.stop() + for connection in connections: + projectr_logger.info("Close connnection %s" % connection) + connection.shutdown(socket.SHUT_RDWR) + connection.close() + tcpprocess.terminate() -if __name__ == "__main__": - process = "Main Process" - logging.info("Christie's Projector") - - # parse command line arguments - parser = argparse.ArgumentParser(description='Project images.') - parser.add_argument("-t", "--test", - help="Run projector in a window for testing", - action="store_true") + # Check if there is a new image to be displayed + if not self.image_queue.empty(): + new_image = self.image_queue.get() + projectr_logger.info("New image is: %s", new_image) + self.pick(new_image) - args = parser.parse_args() +def main(test): # Set up image queue - IMAGEQ = multiprocessing.Queue() - - # Set up connections dict connections = multiprocessing.Manager().dict() - logging.info("Start TCP Receiver") + crsl = Carousel(test) + logger.info("Start TCP Receiver") tcpprocess = multiprocessing.Process(target=tcp_receiver, - args=(IMAGEQ, connections)) + args=(crsl.image_queue, connections)) tcpprocess.start() - logging.info("Start Projector process") - - if args.test: - # If testing, create a small display - DISPLAY = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), - frames_per_second=20, w=800, h=600) - else: - DISPLAY = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), - frames_per_second=20) + crsl.loop(tcpprocess, connections) - SHADER = pi3d.Shader("2d_flat") - alpha_step = 0.025 - crsl = Carousel() - - # Set up camera - CAMERA = pi3d.Camera.instance() - CAMERA.was_moved = False - - # Keyboard - KEYBOARD = pi3d.Keyboard() - - while DISPLAY.loop_running(): - crsl.update() - crsl.draw() - - # Take keyboard events and check for quit - k = KEYBOARD.read() - if k > -1: - if k == 27: - KEYBOARD.close() - DISPLAY.stop() - if len(connections) > 0: - for connection in connections: - print("Close connnection %s" % connection) - connection.shutdown(socket.SHUT_RDWR) - connection.close() - tcpprocess.terminate() - - # Check if there is a new image to be displayed - if not IMAGEQ.empty(): - new_image = IMAGEQ.get() - logging.info("New image is: %s", new_image) - crsl.pick(new_image) +if __name__ == "__main__": + logger.info("Christie's Projector") + parser = argparse.ArgumentParser(description='Project images.') + parser.add_argument("-t", "--test", + help="Run projector in a window for testing", + action="store_true") + args = parser.parse_args() + main(args.test) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6651ee3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +numpy +pi3d +Pillow +flask +flask_sqlalchemy diff --git a/server.py b/server.py index fb57b94..7dd2ea8 100644 --- a/server.py +++ b/server.py @@ -1,630 +1,243 @@ #! /usr/bin/env python """ Web interface for the projector """ - -from __future__ import absolute_import, division, print_function, unicode_literals - -import web +import logging import os -from datetime import datetime -from PIL import Image -import random -import yaml -import sys -import dbus -# Networking -import socket -import pickle -import struct - -# Set up TCP socket port 5005 - -connections = {} - -# Set up URLS - -urls = ( - '/', 'Index', - '/upload', 'Upload', - '/settings', 'Settings', - '/delete', 'Delete', - '/shutdown', 'Shutdown', - '/rename', 'Rename', - '/videos', 'Videos', - '/displays', 'Displays', - '/display/(.+)', 'Index', - '/initnetwork', 'initNetwork' +from flask import Flask, render_template, request, redirect +from flask.views import MethodView +import settings +import services +import networking +import orm +db = orm.db +logging.basicConfig( + filename="server.log", + level=logging.INFO, + format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', ) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) -# Directories -IMAGEDIR = 'static/images/' -VIDEODIR = 'static/videos' -THUMBDIR = 'static/images/thumbs' - - -# Set up web.py app -app = web.application(urls, globals()) - -# Template Renderer -render = web.template.render('templates/', base="layout") - -db = web.database(dbn="sqlite", db="images.db") -# CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); - - -# Image size maximums -WIDTH = 1920 -HEIGHT = 1080 - -""" Functions """ - - -# Use class for settings? -def db_list_images(): - """ List images in database """ - output = db.select('images') - - return output +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' +db.init_app(app) -def db_insert_image(filename): - """ Insert an image into the database """ - imagename, file_extension = os.path.splitext(filename) - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) +CONNECTION = networking.Connection() - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - print(imageid) +class Index(MethodView): + def get(self, display="local"): + if display not in CONNECTION.PROJECTRS.keys(): + logging.info("Display %s not found" % (display)) + return render_template("displaynotfound.html", + pagetitle="Display %s Not Found" % display, + displays=CONNECTION.PROJECTRS, + message="") -def write_settings(data): - """Write the previous image to settings file""" - print("Writing settings...") - print(data) - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) - - -def read_settings(): - """ Read settings from YAML file""" - print("Read settings...") - try: - return yaml.load(open("settings.yml")) - except: - print("Could not read settings") - return None - - -def write_log(logdata, process=""): - """ Write to the log """ - if process == "": - with open("server.log", "a") as myfile: - output = "%s : %s" % (datetime.now().strftime('%Y/%m/%d %H:%M'), - logdata) - myfile.write(output) - print(output) - - else: - with open("server.log", "a") as myfile: - output = "%s : %s : %s" % ( - datetime.now().strftime('%Y/%m/%d %H:%M'), - process, - logdata - ) - myfile.write(output) - print(output) - - -def rename_image(filename): - """ Rename files with random string to ensure there are no clashes """ - randomstring = random.getrandbits(16) - filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] - print(filename) - - -def make_thumbnail(image): - """ Make thumnail for given image """ - # if thumbnail doesn't exist - if not os.path.exists(os.path.join(thumbdir, image)): - imagepath = image - print(get_logtime("%s: PIL - File to open is: %s" % (cur_process, - imagepath))) - try: - # open and convert to RGB - img = Image.open(imagepath).convert('RGB') - - # find ratio of new height to old height - hpercent = (float(HEIGHT) / float(img.size[1])) - # apply ratio to create new width - wsize = int(float(img.size[0]) * hpercent) - # resize image with antialiasing - img = img.resize((int(wsize), int(HEIGHT)), Image.ANTIALIAS) - # save with quality of 80, optimise setting caused crash - img.save(imagepath, format='JPEG', quality=90) - write_log("Sucessfully resized: %s \n" % image) - except IOError: - write_log( - "IO Error. %s will be deleted and " - "downloaded properly next sync" - % imagepath) - os.remove(imagepath) - else: - write_log("Thumbnail for %s exists \n" % image) - - -def list_files(directory, reverse=False): - """ Return list of files of specified type """ - output = [f for f in os.listdir(directory) if - os.path.isfile(os.path.join(directory, f)) and - f.endswith(('.jpg', '.jpeg', '.png'))] - - if reverse is True: - # Sort newFileList by date added(?) - output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) - output.reverse() # reverse image list so new files are first - else: - pass - - return output - - -def connect_display(display): - process = "connect_display" - """ Try and connect to a display - This is a bit of a clusterfuck - should probably re-engineer this """ - # Make a new socket - write_log("Connections: " % connections, process) - try: - write_log("Remove display socket from connections", process) - del connections[display] - except KeyError, e: - write_log("Display doesn't exist in connections", process) - - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - write_log("Socket made", process) - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - # set socket timeout high - connections[display].settimeout(20) - return True - except: - write_log("No displays found", process) - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - return False - - -def send_msg_display(display, msg): - process = "Send Message To Display" - """ Send a message to a display """ - write_log("Send message to display", process) - x = 0 - attempts = 1 - while x == 0 and attempts < 4: - try: - sock = connections[display] - x = 1 - except: - e = sys.exc_info()[0] - write_log("Display %s doesn't exist: %s" % (display, e), process) - - write_log("Reattach display, attempt %d" % attempts, process) - attempt = connect_display(display) - - if attempt is True: - write_log("Successfully reconnected display", process) - else: - write_log("Could not reattach display", process) - attempts += 1 - - if display in connections: - # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg - - # Try sending message, if broken pipe - # Try creating new socket - x = 0 - attempts = 0 - - while x < 1 and attempts < 3: - try: - write_log("Attempting to send message", process) - sock.sendall(msg) - write_log("Sent message %s" % msg) - # success, quit loop - x = 1 - except socket.error, e: - write_log("Socket error: %s" % e, process) - write_log("Attempting to reconnect to display %s" % sock, - process) - write_log(connections) - - # Make a new socket - del connections[display] - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - # set socket timeout high - connections[display].settimeout(20) - except: - write_log("Something fucked up", process) - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - attempts += 1 - else: - write_log("Could not send message, could not communicate with display", - process) - - -def recv_msg(sock): - # Read message length and unpack it into an integer - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - """Helper function to recv n bytes or return None if EOF is hit""" - data = '' - - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: - return None - data += packet - - return data - - -def init_network(connections): - process = "Network Initialisation" - write_log("Check display connections", process) - for display in PROJECTRS: - if PROJECTRS[display]["enabled"] is True: - if display not in connections: - write_log("Display not in existing connections", process) - write_log("Connecting to display %s" % display, process) - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - connections[display].settimeout(1) - - try: - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - connections[display].settimeout(20) - except: - write_log("Could not connect", process) - del connections[display] - else: - send_msg_display(display, "alive") - try: - check = recv_msg(connections[display]) - write_log("Already connected to %s" % display, process) - except: - write_log("Connection appears to be dead", process) - del connections[display] - write_log("Connecting to display %s" % display, process) - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - connections[display].settimeout(1) - - try: - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - connections[display].settimeout(20) - except: - write_log("Something fucked up", process) - del connections[display] - write_log("Connections: %s" % connections, process) - - -# Home Page -class Index(object): - def GET(self, display="local"): - - current_display = display + # current_image = CONNECTION.whatsplaying("local") # FIXME current_image = None - - # Check if the display exists - if current_display in [f for f in PROJECTRS]: - # Ask screen what is currently displayed - message = pickle.dumps({"action": "whatsplaying"}) - send_msg_display(display, message) - - # Wait for reply - try: - current_image = recv_msg(connections[display]) - write_log("Received TCP data: %s from %s" % - (current_image, connections[display])) - except socket.timeout: - write_log("No reply, socket timed out") - except KeyError, e: - write_log("Display not found") - - images = db_list_images() - imagelist = [] - - for image in images: - imagelist.append((image["filename"], image["imagename"])) - - if current_image is None: - return render.index(imagelist, "Display - %s" % - PROJECTRS[current_display]["name"], - current_image, PROJECTRS, "") - else: - return render.index(imagelist, "Display - %s" % - PROJECTRS[current_display]["name"], - current_image[14:], PROJECTRS, "") - else: - write_log("Display %s not found" % (current_display)) - return render.displaynotfound("Display %s Not Found" % - current_display, PROJECTRS, "") - - def POST(self, display="local"): - print("Index POST") - - # Check the action - action = web.input().action # Project? - - # If project, project image + images = orm.Image.query.all() + imagelist = [(x.filename, x.imagename) for x in images] + + return render_template( + "index.html", + imagelist=imagelist, + pagetitle="Display - %s" % CONNECTION.PROJECTRS[display]["name"], + current_image=current_image[14:] if current_image else current_image, + displays=CONNECTION.PROJECTRS, + message="") + + def post(self, display="local"): + action = request.form["action"] if action == "project": - prop1 = web.input().prop1 # THE IMAGE - prop2 = web.input().prop2 - print("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) - message = pickle.dumps({"action": "project", - "images": [os.path.join(IMAGEDIR, prop1)]}) - send_msg_display(display, message) - print("Image to be projected is %s" % prop1) - PROJECTRS[display]["current"] = prop1 + image = request.form["prop1"] + logger.info( + "action: %s, image: %s" % (action, image)) + CONNECTION.project("local", image) return True elif action == "slideshow": - # Get contents of the input - slideshow = web.input() - - imagelist = [] - - # Get list of images - for image in slideshow: - if image != "action": - print(slideshow[image]) - imagelist.append(slideshow[image]) - - write_log(slideshow) - - message = pickle.dumps({"action": "slideshow", "images": "hello"}) - write_log(message) - send_msg_display(display, message) - raise web.seeother('/') + # TODO: Does this work? + imagelist = [ + v for k, v in request.args.iteritems() if k != "action"] + CONNECTION.slideshow("local", imagelist) + return redirect('/') else: - print("Unknown Action %s" % action) - - -class Videos(object): - def GET(self): - # Get folders in users folders - # imagelist = list_files(IMAGEDIR) - - videolist = [f for f in os.listdir(VIDEODIR) if - os.path.isfile(os.path.join(VIDEODIR, f)) and - f.endswith(('.mp4', '.webm'))] - - write_log("User accessed index") - - return render.videos(videolist, "Home", PROJECTRS, "") - - def POST(self): - print("Index POST") - - # Check the action - action = web.input().action # Project? - - # If project, project video - if action == "project": - prop1 = web.input().prop1 # THE video - prop2 = web.input().prop2 - print("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) - message = pickle.dumps({"action": "project", - "video": [os.path.join(VIDEODIR, prop1)]}) - sock.sendto(message, (TCP_IP, TCP_PORT)) - print("video to be projected is %s" % prop1) - return True - else: - print("Unknown Action %s" % action) - - -class initNetwork(object): - def GET(self): - init_network(connections) - raise web.seeother('/') - - -class Settings(object): - def GET(self): - # Get settings - settingsdict = read_settings() - return render.settings("Settings", settingsdict, PROJECTRS, "") - - def POST(self): - newsettings = web.input() - # newsetting is - - settings = read_settings() - - settings["slideshow"]["delay"] = int(newsettings["slideshowlength"]) - write_settings(settings) - - raise web.seeother('/') - - -class Delete(object): - def GET(self): - image = web.input().image - - return render.delete("Delete", PROJECTRS, image) - - def POST(self): - image = web.input().image + logger.info("Unknown Action %s" % action) +app.add_url_rule('/', view_func=Index.as_view('index')) +app.add_url_rule('/display/', view_func=Index.as_view('display')) + + +class initNetwork(MethodView): + def get(self): + networking.init_network() + return redirect('/') +app.add_url_rule('/initnetwork', view_func=initNetwork.as_view('initnetwork')) + + +class Settings(MethodView): + def get(self): + settingsdict = services.read_settings() + return render_template("settings.html", + pagetitle="Settings", + settingsdict=settingsdict, + displays=networking.PROJECTRS, + message="") + + def post(self): + settings = services.read_settings() + settings["slideshow"]["delay"] = int(request.args["slideshowlength"]) + services.write_settings(settings) + return redirect('/') +app.add_url_rule('/settings', view_func=Settings.as_view('settings')) + + +class Delete(MethodView): + def get(self): + image = request.args["image"] + return render_template("delete.html", + pagetitle="Delete", + displays=CONNECTION.PROJECTRS, + image=image) + + def post(self): + print("---------") + print(request.form) + print("---------") + image = request.form["image"] try: - os.remove(os.path.join(IMAGEDIR, image)) - write_log("%s deleted" % image) - except OSError, e: - write_log("Deleting %s failed" % image) - write_log("Error: %s" % e) - - db.delete('images', where="filename=$image", vars=locals()) - - raise web.seeother('/') - - -class Rename(object): - def GET(self): - image = web.input().image - - # Get image name from database - imagename = db.select('images', where="filename=$image", - vars=locals())[0]["imagename"] - - return render.rename("Rename", image, PROJECTRS, imagename) - - def POST(self): - image = web.input().image - newname = web.input().newname - - # Update image in database with new name + os.remove(os.path.join(settings.IMAGEDIR, image)) + logger.info("%s deleted" % image) + except OSError: + logger.exception("Deleting %s failed" % image) + else: + img = db.Image.query.filter_by(imagename=image).one_or_none() + db.session.delete(img) + db.session.commit() + return redirect('/') +app.add_url_rule('/delete', view_func=Delete.as_view('delete')) + + +class Rename(MethodView): + def get(self): + filename = request.args["image"] + imagename = services.db_get_image(filename)[0]["imagename"] + return render_template("rename.html", + pagetitle="Rename", + image=filename, + displays=networking.PROJECTRS, + message=imagename) + + def post(self): + image = request.form["image"] + newname = request.form["newname"] db.update('images', where="filename=$image", imagename=newname, vars=locals()) - - raise web.seeother('/') - - -class Upload(object): - def GET(self): - return render.upload("Upload", PROJECTRS, "") - - def POST(self): - image = web.input(newimage={}) - - if 'newimage' in image: # to check if the file-object is created - # replaces the windows-style slashes with linux ones. - filepath = image.newimage.filename.replace('\\', '/') - # splits the path and chooses the last part (the filename with extension) - imagename = filepath.split('/')[-1] - imagename, file_extension = os.path.splitext(imagename) - imagename = imagename - - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) - - newimagepath = os.path.join(IMAGEDIR, filename) - - # creates the file where the uploaded file should be stored - fout = open(newimagepath, 'w') - # writes the uploaded file to the newly created file. - fout.write(image.newimage.file.read()) - fout.close() # closes the file, upload complete. - write_log("%s uploaded successfully" % imagename) - write_log("Resizing %s" % imagename) - - # Add image to database - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - print("Added %s to the database as ID %d" % (imagename, imageid)) - - try: - # open and convert to RGB - img = Image.open(newimagepath).convert('RGB') - - # find ratio of new height to old height - hpercent = (float(HEIGHT) / float(img.size[1])) - # apply ratio to create new width - wsize = int(float(img.size[0]) * hpercent) - # resize image with antialiasing - img = img.resize((int(wsize), int(HEIGHT)), Image.ANTIALIAS) - # save with quality of 80, optimise setting caused crash - # Delete original (in case it is a different filetype) - # This needs to be changed! - os.remove(newimagepath) - newimagepath = os.path.splitext(newimagepath)[0] + ".jpg" - img.save(newimagepath, format='JPEG', quality=90) - write_log("Sucessfully resized: %s \n" % newimagepath) - except IOError: - write_log("IO Error. %s will be deleted and downloaded properly next sync" % newimagepath) - os.remove(newimagepath) - - raise web.seeother('/') - - -class Displays(object): - def GET(self): - displays = PROJECTRS - print(connections) - alive = [f for f in connections] - - return render.displays("Displays", displays, alive, "") - - def POST(self): - display = web.input().prop1 - message = pickle.dumps({"action": "sync"}) - - sock.sendto(message, (PROJECTRS[display]["ip"], - PROJECTRS[display]["port"])) - - print("sync %s" % display) - - -class Shutdown(object): - def GET(self): - return render.shutdown("Shutdown?", PROJECTRS, "") - - def POST(self): - shutdown = web.input().shutdown - print(shutdown) + return redirect('/') +app.add_url_rule('/rename', view_func=Rename.as_view('rename')) + + +class Upload(MethodView): + def get(self): + return render_template("upload.html", + pagetitle="Upload", + displays=CONNECTION.PROJECTRS, + message="") + + def post(self): + if 'newimage' not in request.files: + return + + newimage = request.files["newimage"] + # replaces the windows-style slashes with linux ones. + filepath = newimage.filename.replace('\\', '/') + # splits the path and chooses the last part (the filename with extension) + filename = filepath.split('/')[-1] + imagename, file_extension = os.path.splitext(filename) + filename = services.rename_image(filename) + image = orm.Image(filename=filename, imagename=imagename) + db.session.add(image) + db.session.commit() + + newimagepath = os.path.join(settings.IMAGEDIR, filename) + newimage.save(newimagepath) + services.make_thumbnail(newimagepath) + return redirect('/') +app.add_url_rule('/upload', view_func=Upload.as_view('upload')) + + +class Displays(MethodView): + def get(self): + logger.info(networking.connections) + alive = [f for f in networking.connections] + + return render_template("displays.html", + pagetitle="Displays", + displays=networking.PROJECTRS, + alive=alive, + message="") + + def post(self): + display = request.args["prop1"] + message = {"action": "sync"} + networking.send_msg_to_display(display, message) + logger.info("sync %s" % display) +app.add_url_rule('/displays', view_func=Displays.as_view('displays')) + + +class Shutdown(MethodView): + def get(self): + return render_template("shutdown.html", + pagetitle="Shutdown?", + displays=networking.PROJECTRS, + message="") + + def post(self): + shutdown = request.form["shutdown"] + logger.info(shutdown) if shutdown == "true": - print("Shutdown") - message = pickle.dumps({"action": "project", - "images": ['static/img/shutdown.jpg']}) + logger.info("Shutdown") + message = {"action": "project", + "images": ['static/img/shutdown.jpg']} # Should probably cycle through display and switch them off - send_msg_display("local", message) + networking.send_msg_to_display("local", message) os.system("poweroff") else: - print("Don't shutdown") - raise web.seeother('/') + logger.info("Don't shutdown") + return redirect('/') +app.add_url_rule('/shutdown', view_func=Shutdown.as_view('shutdown')) + + +class Videos(MethodView): + def get(self): + videolist = services.list_files(settings.VIDEODIR, video=True) + logger.info("User accessed index") + return render_template("videos.html", + videolist=videolist, + pagetitle="Home", + displays=networking.PROJECTRS, + message="") + + def post(self, display="local"): + # Check the action + action = request.args["action"] + # If project, project video + if action == "project": + prop1 = request.args["prop1"] # THE video + prop2 = request.args["prop2"] + logger.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) + message = {"action": "project", + "video": [os.path.join(settings.VIDEODIR, prop1)]} + networking.send_msg_to_display(display, message) + logger.info("video to be projected is %s" % prop1) + return True + else: + logger.info("Unknown Action %s" % action) +app.add_url_rule('/videos', view_func=Videos.as_view('videos')) -SETTINGS = read_settings() -PROJECTRS = SETTINGS["projectors"] if __name__ == "__main__": - # Set up settings - - init_network(connections) - app.run() + CONNECTION.init_network() + app.run(host="localhost", port=8000) diff --git a/services.py b/services.py new file mode 100644 index 0000000..0e2e4fd --- /dev/null +++ b/services.py @@ -0,0 +1,113 @@ +import logging +import os +import random + +import json +from PIL import Image +import settings + + +def db_get_image(db, filename): + return db.select( + 'images', + where="filename=$filename", + vars=locals()) + + +def db_delete_image(db, filename): + db.delete('images', where="filename=$filename", vars=locals()) + + +def db_list_images(db, filename): + """ List images in database """ + return db.select('images') + + +def rename_image(filename): + """ Rename files with random string to ensure there are no clashes """ + randomstring = random.getrandbits(16) + filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] + logging.info(filename) + return filename + + +def write_settings(data, settings_file='uisettings.json'): + """Write the previous image to settings file""" + logging.info("Writing settings...") + logging.info(data) + with open(settings_file, 'w') as outfile: + json.dump(data, outfile) + + +def read_settings(settings_file='uisettings.json', default_settings=None): + """ Read settings from YAML file""" + logging.info("Read settings...") + try: + with open(settings_file) as infile: + data = json.load(infile) + return data + except IOError: + logging.exception("Could not read settings") + if default_settings: + logging.info("Writing default settings file") + with open(settings_file, 'w') as outfile: + json.dump(default_settings, outfile) + return default_settings + except ValueError: + logging.exception("Could not read settings") + return None + + +def update_setting(setting_name, value, file_name='uisettings.json'): + settings = read_settings(settings_file=file_name) + settings[setting_name] = value + write_settings(settings, settings_file=file_name) + + +def get_setting(setting_name, file_name='uisettings.json'): + settings = read_settings(settings_file=file_name) + return settings[setting_name] + + +def make_thumbnail(imagepath): + """ Make thumnail for given image """ + # if thumbnail doesn't exist + if os.path.exists(os.path.join(settings.THUMBDIR, imagepath)): + logging.info("Thumbnail for %s exists \n" % imagepath) + return + + logging.info("PIL - File to open is: %s" % imagepath) + try: + # open and convert to RGB + img = Image.open(imagepath).convert('RGB') + + # find ratio of new height to old height + hpercent = (float(settings.HEIGHT) / float(img.size[1])) + # apply ratio to create new width + wsize = int(float(img.size[0]) * hpercent) + # resize image with antialiasing + img = img.resize((int(wsize), int(settings.HEIGHT)), Image.ANTIALIAS) + # save with quality of 80, optimise setting caused crash + img.save(imagepath, format='JPEG', quality=90) + logging.info("Sucessfully resized: %s \n" % imagepath) + except IOError: + logging.info( + "IO Error. %s will be deleted and " + "downloaded properly next sync" + % imagepath) + os.remove(imagepath) + + +def list_files(directory, reverse=False, video=False): + """ Return list of files of specified type """ + filetypes = ('.mp4', '.webm') if video else ('.jpg', '.jpeg', '.png') + output = [f for f in os.listdir(directory) if + os.path.isfile(os.path.join(directory, f)) and + f.endswith(filetypes)] + + if reverse: + # Sort newFileList by date added(?) + output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) + output.reverse() # reverse image list so new files are first + + return output diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..6de4557 --- /dev/null +++ b/settings.json @@ -0,0 +1 @@ +{"slideshow": {"delay": 20, "loop": true}, "fadeduration": 2, "lastimage": "static/images/logo.jpg", "projectors": {"local": {"ip": "127.0.0.1", "port": 5006, "enabled": true, "name": "Main", "current": ""}}} \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..d876304 --- /dev/null +++ b/settings.py @@ -0,0 +1,8 @@ +# Directories +IMAGEDIR = 'static/images/' +VIDEODIR = 'static/videos' +THUMBDIR = 'static/images/thumbs' + +# Image size maximums +WIDTH = 1920 +HEIGHT = 1080 diff --git a/static/js/functions.js b/static/js/functions.js index 23e3838..dfa02cb 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -2,7 +2,6 @@ jQuery(document).ready(function() { jQuery(function( $ ) { $('#displayswitch').on('click', function(){ - if ($('#displayslist').css('display') === 'none'){ $('#displayslist').slideDown(); } else { diff --git a/static/js/functionsold.js b/static/js/functionsold.js deleted file mode 100644 index 11bef72..0000000 --- a/static/js/functionsold.js +++ /dev/null @@ -1,69 +0,0 @@ -jQuery(document).ready(function() { -jQuery(function( $ ) { - - $('.delete').on('click', function() { - - image = $(this).parents('.tile').data("image"); - //alert(absimage); - - - current_element = $(this).parents('.tile'); - - nothing = "nothing"; - - jQuery.ajax({ - type: "POST", - data: {action : "delete", prop1 : image, prop2 : nothing}, - success: function() { - current_element.css("display", "none"); - } - }); - return false; - }); - - $('.project').on('click', function() { - image = $(this).parents('.tile').data("image"); - - - nothing = "nothing"; - - jQuery.ajax({ - type:"POST", - data: {action: "project", prop1 : image, prop2: nothing}, - success: function() { - alert("Projecting " + image); - } - }); - - }); - - $('#menubutton').on('click', function(){ - - if ($('nav').css('display') === 'none'){ - $('nav').css("display", "block"); - } else { - $('nav').css("display", "none"); - } - - }); - -}); - -}); - -/* -$("id-" + image).attr("src", "../" + absimage + "?" + d.getTime()); - -jQuery(document).ready(function() { -jQuery(".button").click(function() { - var input_string = $$("input#textfield").val(); - jQuery.ajax({ - type: "POST", - data: {textfield : input_string}, - success: function(data) { - jQuery('#foo').html(data).hide().fadeIn(1500); - }, - }); - return false; - }); -});*/ \ No newline at end of file diff --git a/templates/delete.html b/templates/delete.html index 3fac0ab..92f371d 100644 --- a/templates/delete.html +++ b/templates/delete.html @@ -1,14 +1,11 @@ -$def with (pagetitle, displays, image) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}
- - Delete $image? + + Delete {{image}}?

-
\ No newline at end of file + +{% endblock %} diff --git a/templates/displaynotfound.html b/templates/displaynotfound.html index 6db68a5..3a809b5 100644 --- a/templates/displaynotfound.html +++ b/templates/displaynotfound.html @@ -1,7 +1,6 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays +{% extends 'layout.html' %} +{% block content %}

DISPLAY NOT FOUND

-

Edit Displays

\ No newline at end of file +

Edit Displays

+{% endblock %} \ No newline at end of file diff --git a/templates/displays.html b/templates/displays.html index cfe2872..bd7e591 100644 --- a/templates/displays.html +++ b/templates/displays.html @@ -1,10 +1,5 @@ -$def with (pagetitle, displays, alive, message) - -$var pagetitle = pagetitle -$var displays = displays - -$alive - +{% extends 'layout.html' %} +{% block content %} New Static Display
@@ -17,35 +12,39 @@ Alive? Remove - $for display in displays: + {% for display_name, display in displays.items() %} - $displays[display]["name"] - $displays[display]["ip"] - O + {{display.name}} + {{display.ip}} + O - $if display in alive: + {%if display in alive %} Yes - $else: + {% else %} No + {% endif %} + {% endfor %}
-$for display in displays: - $if display in alive: +{% for display_name, display in displays.items() %} + {% if display in alive %}
- $else: + {% else %}
- - + {% endif %} + +
- $displays[display]['name']
- $displays[display]['ip'] + {{display_name}}
+ {{display.ip}}
-
-
\ No newline at end of file +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4294cd6..a4e4c97 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,11 +1,5 @@ -$def with (imagelist, pagetitle, current_image, displays, message) - -$if current_image == "unknown": - $var pagetitle = "%s - X" % pagetitle -$else: - $var pagetitle = pagetitle -$var displays = displays - +{% extends 'layout.html' %} +{% block content %}
@@ -13,24 +7,25 @@
-$for image in imagelist: - $if image[0] == current_image: -
- $else: -
- - +{% for image in imagelist %} + {% if image[0] == current_image %} +
+ {% else %} +
+ {% endif %} +
-
Delete
-
Rename
+
Delete
+
Rename
Project
- +
- - - -
+ +
+{% endfor %}
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/indexold.html b/templates/indexold.html deleted file mode 100644 index 836ed3c..0000000 --- a/templates/indexold.html +++ /dev/null @@ -1,28 +0,0 @@ -$def with (imagelist, pagetitle, message) - -$var pagetitle = pagetitle - -
-
- - - -
- -$for image in imagelist: -
- -
-
Delete
-
Rename
-
Project
- -
- - - -
- -
-
-
\ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index e6bc232..63548bf 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,4 +1,3 @@ -$def with (content) @@ -20,9 +19,8 @@
- - $:content + + {% block content %} + {% endblock %}
diff --git a/templates/layoutold.html b/templates/layoutold.html deleted file mode 100644 index 134393c..0000000 --- a/templates/layoutold.html +++ /dev/null @@ -1,41 +0,0 @@ -$def with (content) - - - - Projectr - - - - - - - - - -
- - -
- - $:content - -
-
- - \ No newline at end of file diff --git a/templates/rename.html b/templates/rename.html index 9e2b1dc..84ac417 100644 --- a/templates/rename.html +++ b/templates/rename.html @@ -1,16 +1,12 @@ -$def with (pagetitle, image, displays, imagename) - -$var pagetitle = pagetitle -$var displays = displays - - - +{% extends 'layout.html' %} +{% block content %}
- - Rename $imagename?

+ + Rename {{imagename}}?



-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html index eb47950..bc9f4c0 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1,12 +1,10 @@ -$def with (pagetitle, settingsdict, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - -
+{% extends 'layout.html' %} +{% block content %} +

Maximum slide time is 17 seconds




-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/shutdown.html b/templates/shutdown.html index 17c06dc..06e83c2 100644 --- a/templates/shutdown.html +++ b/templates/shutdown.html @@ -1,13 +1,10 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/upload.html b/templates/upload.html index 0b402d9..a79cdd3 100644 --- a/templates/upload.html +++ b/templates/upload.html @@ -1,13 +1,10 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}

Please avoid any files with special characters in their names.



-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/videos.html b/templates/videos.html index 3027910..caa09c7 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -1,7 +1,5 @@ -$def with (videolist, pagetitle, message) - -$var pagetitle = pagetitle - +{% extends 'layout.html' %} +{% block content %}
@@ -24,4 +22,5 @@
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/tests/client.py b/tests/client.py deleted file mode 100644 index fc8c8cb..0000000 --- a/tests/client.py +++ /dev/null @@ -1,43 +0,0 @@ -import socket -import time -import fcntl -import struct - - -PORT = 50000 -MAGIC = "sdf876sd" #to make sure we don't confuse or get confused by other programs - -foundclients = False - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #create UDP socket -s.bind(('', 0)) -s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #this is a broadcast socket - -def get_ip_address(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) - -my_ip = get_ip_address('wlan0') - -while 1: - data = MAGIC+my_ip - s.sendto(data, ('', PORT)) - print "sent service announcement, waiting for reply" - - # wait for reply, timeout after 1 minute - data, addr = s.recvfrom(1024) - print data - - # when receive reply, break loop and continue with IP - if data == "hello": - print "Received acknowledgement from server" - break - -print "Escaped successfully" - - - diff --git a/tests/getip.py b/tests/getip.py deleted file mode 100644 index 0ff7a61..0000000 --- a/tests/getip.py +++ /dev/null @@ -1,12 +0,0 @@ -import socket -import time -import fcntl -import struct - -def get_ip_address(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) \ No newline at end of file diff --git a/tests/projector.py b/tests/projector.py deleted file mode 100644 index 834c6c6..0000000 --- a/tests/projector.py +++ /dev/null @@ -1,14 +0,0 @@ -import socket - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP -sock.bind((UDP_IP, UDP_PORT)) - -while True: - data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes - print "received messages:", data - print "From address: ", addr \ No newline at end of file diff --git a/tests/randomname.py b/tests/randomname.py deleted file mode 100644 index 9c75cbc..0000000 --- a/tests/randomname.py +++ /dev/null @@ -1,8 +0,0 @@ -import random - -def rename_image(filename): - randomstring = random.getrandbits(16) - filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] - print filename - -rename_image('/christie/file.jpg') \ No newline at end of file diff --git a/tests/server.py b/tests/server.py deleted file mode 100644 index 190d9a8..0000000 --- a/tests/server.py +++ /dev/null @@ -1,21 +0,0 @@ -import socket - -PORT = 50000 -MAGIC = "sdf876sd" # make us unique - - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #create UDP socket -s.bind(('', PORT)) - -clients = {} - -while 1: - print "Waiting for service announcement" - data, addr = s.recvfrom(1024) # wait for a packet - if data.startswith(MAGIC): - clientip = data[len(MAGIC):] - print clientip - print "got service announcement from", data[len(MAGIC):] - # Tell client we've heard - print "Sending reply" - s.sendto("hello", (clientip, PORT)) diff --git a/tests/show_logo.py b/tests/show_logo.py deleted file mode 100644 index 677f208..0000000 --- a/tests/show_logo.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import pygame -import time -import socket -import sys - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -# Set up UDP -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP -sock.bind((UDP_IP, UDP_PORT)) - - -# check if there is an X display -disp_no = os.getenv('DISPLAY') - -if disp_no: - print "I'm running under X display = {0}".format(disp_no) - -# List of possible Framebuffer drivers, directfb is preferable -drivers = ['directfb', 'fbcon', 'svgalib'] - -found = False -# attempt to use each driver with pygame -for driver in drivers: - if not os.getenv('SDL_VIDEODRIVER'): - os.putenv('SDL_VIDEODRIVER', driver) - print driver - try: - pygame.display.init() - except pygame.error: - print 'Driver: {0} failed.'.format(driver) - continue - found = True - break - -if not found: - raise Exception('No suitable video driver found!') - - -size = (pygame.display.Info().current_w, pygame.display.Info().current_h) -screen = pygame.display.set_mode(size, pygame.FULLSCREEN) - -IMG = pygame.image.load("/home/pi/server/static/images/logo.png") - -while True: - events = pygame.event.get() - for event in pygame.event.get(): - if event.type == QUIT: - pygame.quit() - sys.exit() - - x = (size[0] / 2) - (IMG.get_rect().size[0] / 2) - - pygame.mouse.set_visible(False) - screen.fill((0,0,0)) - screen.blit(IMG, (x, 0)) - pygame.display.flip() - - # Wait for udp - DATA, ADDR = sock.recvfrom(1024) # buffer size is 1024 bytes - try: - IMG = pygame.image.load("/home/pi/server/static/images/%s" % DATA) - except: - print "ERROR! Could not load image %s" % DATA - - # Check if new image is bigger than screen - if (IMG.get_rect().size[0] > size[0]) or IMG.get_rect().size[1] > size[1]: - IMG = pygame.transform.smoothscale(IMG, (size[0], IMG.get_rect().size[1] / (IMG.get_rect().size[0] / size[0]))) - # Check if image is still too tall after being resized - if IMG.get_rect().size[1] > size[1]: - IMG = pygame.transform.smoothscale(IMG, (IMG.get_rect().size[0] / (IMG.get_rect().size[1] / size[1]), size[1])) - else: - pass diff --git a/tests/udpsend.py b/tests/udpsend.py deleted file mode 100644 index 9746c62..0000000 --- a/tests/udpsend.py +++ /dev/null @@ -1,16 +0,0 @@ -import socket - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -SOCK = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP - -#print "starting at: %d" % count - -print "Input filename of image to show:" -IMAGE = raw_input("> ") - -# send image name to projector -SOCK.sendto(IMAGE, (UDP_IP, UDP_PORT)) diff --git a/uisettings.yml b/uisettings.yml new file mode 100644 index 0000000..f1bddfc --- /dev/null +++ b/uisettings.yml @@ -0,0 +1,5 @@ +{!!python/unicode 'fadeduration': 2, !!python/unicode 'lastimage': !!python/unicode 'static/images/logo.jpg', + !!python/unicode 'projectors': {!!python/unicode 'local': {!!python/unicode 'current': !!python/unicode '', + !!python/unicode 'enabled': true, !!python/unicode 'ip': !!python/unicode '127.0.0.1', + !!python/unicode 'name': !!python/unicode 'Main', !!python/unicode 'port': 5006}}, + !!python/unicode 'slideshow': {!!python/unicode 'delay': 20, !!python/unicode 'loop': true}}