commit d6a44a2b72bb7de1bcbdf9b9af8583c64a27ab8e
Author: David Sorber <david.sorber@gmail.com>
Date:   Sat Jan 5 15:06:58 2019 -0500

    Adding very basic command server and module packaging.

diff --git a/software/hbkcd/hbkcd.py b/software/hbkcd/hbkcd.py
deleted file mode 100755
index 3aae1c0..0000000
--- a/software/hbkcd/hbkcd.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/python3
-import logging
-import os
-import sys
-
-#
-# HbkCD - Handbrake Control Daemon
-#
-
-###############################################################################
-# Configuration Options
-###############################################################################
-LOGFILE_PATH = '/var/log/hbkcd/hbkcd.log'
-LOG_FORMAT_STR = '%(asctime)s|%(levelname)s|%(filename)s:%(lineno)s|%(message)s'
-LOG_DATE_FORMAT_STR = '%a %b %d %H:%M:%S %Y'
-DEFAULT_LOG_LEVEL = logging.DEBUG
-
-# Absolute path the to the HandbrakeCLI executable
-HANDBRAKE_PATH = '/opt/local/bin/HandBrakeCLI'
-
-# Path to queue file. Note that there is nothing special about this file, it
-# is simply a text file containing a list of paths to video files to re-enocde
-QUEUE_FILE_PATH = '/home/dsorber/encode_queue'
-
-def check_configuration():
-    """ Check runtime parameters for correctness before attempting to use them.
-    """
-    
-    # Make sure specified HandbrakeCLI path exists and is a file
-    if not os.path.isfile(HANDBRAKE_PATH):
-        logging.error('Handbrake does not exist at path: {:s}'
-                      .format(HANDBRAKE_PATH))
-        return False
-    
-    # Make sure the specified HandbrakeCLI file is executable
-    if not os.access(HANDBRAKE_PATH, os.X_OK):
-        logging.error('Handbrake exists but is not executable ({:s})'
-                      .format(HANDBRAKE_PATH))
-        return False
-        
-    # Make sure specified encode queue file exists and is a file
-    if not os.path.isfile(QUEUE_FILE_PATH):
-        logging.error('The specified encode queue file ({:s}) does not exist, '
-                      'please create it to proceed'.format(QUEUE_FILE_PATH))
-        return False
-    
-    logging.debug('Runtime configuration checks OKAY')
-    
-    return True
-    
-
-def main():
-
-    # Configure logging before attempting anything serious
-    logging.basicConfig(format=LOG_FORMAT_STR, 
-                        datefmt=LOG_DATE_FORMAT_STR, 
-                        # ~ filename=LOGFILE_PATH, 
-                        level=DEFAULT_LOG_LEVEL)
-    
-    # ~ print('foobar', file=sys.stderr)
-    logging.info('hbkcf started')
-    
-    # Validate runtime configuration before proceeding
-    if not check_configuration():
-        return -1
-    
-    return 0
-    
-if __name__ == '__main__':
-    sys.exit(main())
diff --git a/software/hbkcd/hbkcd/__init__.py b/software/hbkcd/hbkcd/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/software/hbkcd/hbkcd/command_server.py b/software/hbkcd/hbkcd/command_server.py
new file mode 100755
index 0000000..979ad93
--- /dev/null
+++ b/software/hbkcd/hbkcd/command_server.py
@@ -0,0 +1,154 @@
+#!/usr/bin/python3
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import logging
+import json
+import os
+import sys
+import time
+import threading
+
+from hbkcd.queue import ENCODE_QUEUE
+
+# https://blog.gocept.com/2011/08/04/shutting-down-an-httpserver/
+# https://docs.aws.amazon.com/polly/latest/dg/example-Python-server-code.html
+
+class StoppableHTTPServer(HTTPServer):
+    
+    def run(self):
+        try:
+            self.serve_forever()
+        except KeyboardInterrupt:
+            pass
+        finally:
+            # Clean-up server (close socket, etc.)
+            self.server_close()
+ 
+
+class CommandHandler(BaseHTTPRequestHandler):
+ 
+    def log_message(format, *args):        
+        logging.debug('Request: {} -- {}'.format(args[1], args[2]))
+        # ~ print('REQUEST: {} -- {}'.format(args[1], args[2]))
+ 
+    def do_GET(self):
+        """ Handle HTTP GET requests
+        """
+        # Strip off leading and/or trailing slashes
+        path = self.path.strip('/')
+        
+        # Defaults
+        response_code = 404
+        response_type = 'text/html'
+        response_data = ''
+      
+        # Send response status code
+        if path == 'queue':
+            response_code = 200
+            response_type = 'application/json'
+            response_dict = {'queue': ENCODE_QUEUE}
+            response_data = json.dumps(response_dict).encode('utf-8')
+        
+        elif not path:
+            response_code = 200
+            response = 'HandBrake Control Daemon - control interface'
+            response_data = response.encode('utf-8')
+        
+        # Send response and data, if any
+        self.send_response(response_code)
+        
+        if response_data:
+            self.send_header('Content-type', response_type)
+            
+        self.end_headers()
+        
+        if response_data:
+            self.wfile.write(response_data)
+            
+        return
+        
+    def do_POST(self):
+        """ Handle HTTP POST requests
+        """
+        # Strip off leading and/or trailing slashes
+        path = self.path.strip('/')
+        
+        # Defaults
+        response_code = 200
+        response_type = 'application/json'
+        response_dict = {'success': False, 'message': 'Invalid request'}
+        
+        # Only attempt to parse things sent to /command
+        if path == 'command':
+            try:
+                content_length = int(self.headers['Content-Length'])
+                raw_content = self.rfile.read(content_length)
+                # ~ print(type(raw_content))
+                content = json.loads(raw_content)
+                
+                # Default to invalid command
+                response_dict['message'] = 'Command format invalid'
+                
+                if 'type' in content:
+                    # Delegate potentiall valid commands for additional handling
+                    if content['type'] == 'add':
+                        response = self.handle_add_cmd(content)
+                        response_dict['success'] = response[0]
+                        response_dict['message'] = response[1]
+                    
+                
+            except Exception as exp:
+                response_dict['success'] = False
+                error = '{:s}: {!r}'.format(type(exp).__name__, exp.args)
+                response_dict['message'] = error
+        
+        # Send response
+        self.send_response(response_code)
+        self.send_header('Content-type', response_type)
+        self.end_headers()
+        self.wfile.write(json.dumps(response_dict).encode('utf-8'))
+            
+        return
+
+    def handle_add_cmd(self, command_data):
+        """ Handler for the "add" command
+        """
+        if 'path' not in command_data:
+            return False, 'Command format invalid'
+        
+        # Check if the path actually exists:
+        path = command_data['path']
+        if not os.path.isfile(path):
+            return False, 'File does not exist: {:s}'.format(path)
+        
+        # TODO: check if file is video file
+        # TODO: get width, heigh and frame rate using ffprobe
+        
+        ENCODE_QUEUE.append(path)
+        
+        return True, 'Added to queue'
+        
+ 
+def main():
+    print('starting server...')
+ 
+    server = StoppableHTTPServer(('', 8081), CommandHandler) 
+    
+    # Start processing requests
+    thread = threading.Thread(None, server.run)
+    thread.start()
+
+    # Run fo' eva'
+    try:
+        while True:
+            time.sleep(1)
+    except KeyboardInterrupt:
+        print('[server interrupted]')
+    finally:
+        # Shutdown server
+        server.shutdown()
+        thread.join()
+    
+    print('fin')
+ 
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/software/hbkcd/hbkcd/daemon.py b/software/hbkcd/hbkcd/daemon.py
new file mode 100755
index 0000000..eb479a6
--- /dev/null
+++ b/software/hbkcd/hbkcd/daemon.py
@@ -0,0 +1,90 @@
+#!/usr/bin/python3
+import logging
+import os
+import sys
+import threading
+import time
+
+from hbkcd.command_server import StoppableHTTPServer, CommandHandler
+from hbkcd.queue import ENCODE_QUEUE
+
+#
+# HbkCD - Handbrake Control Daemon
+#
+
+###############################################################################
+# Configuration Options
+###############################################################################
+LOGFILE_PATH = '/var/log/hbkcd/hbkcd.log'
+LOG_FORMAT_STR = '%(asctime)s|%(levelname)s|%(filename)s:%(lineno)s|%(message)s'
+LOG_DATE_FORMAT_STR = '%a %b %d %H:%M:%S %Y'
+DEFAULT_LOG_LEVEL = logging.DEBUG
+
+# Absolute path the to the HandbrakeCLI executable
+HANDBRAKE_PATH = '/opt/local/bin/HandBrakeCLI'
+
+# Command server port
+COMMAND_SERVER_PORT = 8081
+
+
+def check_configuration():
+    """ Check runtime parameters for correctness before attempting to use them.
+    """
+    
+    # Make sure specified HandbrakeCLI path exists and is a file
+    if not os.path.isfile(HANDBRAKE_PATH):
+        logging.error('Handbrake does not exist at path: {:s}'
+                      .format(HANDBRAKE_PATH))
+        return False
+    
+    # Make sure the specified HandbrakeCLI file is executable
+    if not os.access(HANDBRAKE_PATH, os.X_OK):
+        logging.error('Handbrake exists but is not executable ({:s})'
+                      .format(HANDBRAKE_PATH))
+        return False
+    
+    logging.debug('Runtime configuration checks OKAY')
+    
+    return True
+    
+
+def main():
+
+    # Configure logging before attempting anything serious
+    logging.basicConfig(format=LOG_FORMAT_STR, 
+                        datefmt=LOG_DATE_FORMAT_STR, 
+                        # ~ filename=LOGFILE_PATH, 
+                        level=DEFAULT_LOG_LEVEL)
+    
+    logging.info('hbkcd started')
+    
+    # Validate runtime configuration before proceeding
+    if not check_configuration():
+        return -1
+    
+    # Create the command server
+    logging.info('Starting command server')
+    server = StoppableHTTPServer(('', COMMAND_SERVER_PORT), CommandHandler) 
+    
+    # Start the command server in a separate "thread"
+    thread = threading.Thread(None, server.run)
+    thread.start()
+    logging.info('Command server running')
+    
+    # Run the command server until CTRL-C'ed
+    try:
+        while True:
+            time.sleep(1)
+    except KeyboardInterrupt:
+        logging.warn('Command server interrupted')
+    finally:
+        # Shutdown server
+        server.shutdown()
+        thread.join()
+        logging.info('Command server stopped')
+    
+    logging.info('hbkcd stopping')
+    return 0
+    
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/software/hbkcd/hbkcd/queue.py b/software/hbkcd/hbkcd/queue.py
new file mode 100644
index 0000000..92bbb97
--- /dev/null
+++ b/software/hbkcd/hbkcd/queue.py
@@ -0,0 +1,7 @@
+
+ENCODE_QUEUE = []
+
+class QueueItem(object):
+
+    def __init__(self, path):
+        self.path = path
diff --git a/software/hbkcd/server.py b/software/hbkcd/server.py
deleted file mode 100755
index 759d13c..0000000
--- a/software/hbkcd/server.py
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/python3
-from http.server import BaseHTTPRequestHandler, HTTPServer
-import json
-from multiprocessing import Process
-import sys
-import time
-
-# https://blog.gocept.com/2011/08/04/shutting-down-an-httpserver/
-# https://docs.aws.amazon.com/polly/latest/dg/example-Python-server-code.html
- 
-class CommandServer(Process):
-    
-    def __init__(self):
-        Process.__init__(self)
-        
-        self.server_address = ('127.0.0.1', 8081)
-        self.httpd = HTTPServer(self.server_address, CommandHandler)
-        self.keep_running = True
-    
-    def stop(self):
-        print('stop called')
-        self.keep_running = False
-        # ~ if self.httpd:
-            # ~ print('pre shutdown')
-            # ~ self.httpd.shutdown()
-            # ~ print('post shutdown')
-        self.httpd.shutdown()
-        self.httpd.socket.close()
-        # ~ self.httpd.server_close()
-        print('stop returning')
-        
-    def run(self):
-        print('command server running')
-        
-        # ~ while self.keep_running:
-        self.httpd.serve_forever()
-        
-        # ~ while self.keep_running:
-            # ~ self.httpd.handle_request()
-        
-        print('command server stopping')
-    
- 
-# HTTPRequestHandler class
-class CommandHandler(BaseHTTPRequestHandler):
- 
-    # GET
-    def do_GET(self):
-        # Strip off leading and/or trailing slashes
-        path = self.path.strip('/')
-        
-        # Defaults
-        response_code = 400
-        response_type = 'text/html'
-        response_data = ''
-      
-        # Send response status code
-        if path == 'queue':
-            response_code = 200
-            response_dict = {'queue': ['alpha', 'bravo', 'charlie']}
-            response_data = json.dumps(response_dict).encode()
-        
-        elif not path:
-            response_code = 200
-            response = 'Why hello there'
-            response_data = response.encode('utf-8')
-        
-        # Send response and data, if any
-        self.send_response(response_code)
-        
-        if response_data:
-            self.send_header('Content-type', response_type)
-            
-        self.end_headers()
-        
-        if response_data:
-            self.wfile.write(response_data)
-            
-        return
-        
- 
-def main():
-    print('starting server...')
- 
-    # Server settings
-    # Choose port 8080, for port 80, which is normally used for a http server, 
-    # you need root access
-    # ~ server_address = ('127.0.0.1', 8081)
-    # ~ httpd = HTTPServer(server_address, CommandHandler)
-    # ~ print('running server...')
-    # ~ httpd.serve_forever()
-    
-    cmd_server = CommandServer()
-    cmd_server.start()
-    
-    # Run for a while 
-    for idx in range(20):
-        print(idx)
-        time.sleep(1)
-        
-    # Simluate a clean shutdown
-    cmd_server.stop()
-    print('post stop')
-    cmd_server.join()
-        
-    print('fin')
- 
-if __name__ == '__main__':
-    sys.exit(main())
diff --git a/software/hbkcd/setup.py b/software/hbkcd/setup.py
new file mode 100755
index 0000000..7834cb4
--- /dev/null
+++ b/software/hbkcd/setup.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python3
+from setuptools import setup, find_packages
+
+setup(
+    name = 'hbkcd',
+    version = '0.0.1',
+    description = 'HandBrake Control Daemon',
+    packages = find_packages(),
+)
+    
