commit 3ce1c60965ae0c21b64d2edd4fab0c923be8a8e9
Author: David Sorber <david.sorber@gmail.com>
Date:   Sat Jan 5 17:40:31 2019 -0500

    Greatly improved the queue.

diff --git a/software/hbkcd/hbkcd/command_server.py b/software/hbkcd/hbkcd/command_server.py
index 979ad93..04e12d3 100755
--- a/software/hbkcd/hbkcd/command_server.py
+++ b/software/hbkcd/hbkcd/command_server.py
@@ -7,7 +7,9 @@ import sys
 import time
 import threading
 
-from hbkcd.queue import ENCODE_QUEUE
+import magic
+
+from hbkcd.queue import ENCODE_QUEUE, QueueItem
 
 # https://blog.gocept.com/2011/08/04/shutting-down-an-httpserver/
 # https://docs.aws.amazon.com/polly/latest/dg/example-Python-server-code.html
@@ -45,7 +47,7 @@ class CommandHandler(BaseHTTPRequestHandler):
         if path == 'queue':
             response_code = 200
             response_type = 'application/json'
-            response_dict = {'queue': ENCODE_QUEUE}
+            response_dict = {'queue': ENCODE_QUEUE.get_list()}
             response_data = json.dumps(response_dict).encode('utf-8')
         
         elif not path:
@@ -95,11 +97,11 @@ class CommandHandler(BaseHTTPRequestHandler):
                         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)
+                error = '{:s}: {!r}'.format(type(exp).__name__, exp.args)                
                 response_dict['message'] = error
+                logging.error(error)
         
         # Send response
         self.send_response(response_code)
@@ -118,12 +120,28 @@ class CommandHandler(BaseHTTPRequestHandler):
         # 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)
+            msg = 'File does not exist: {:s}'.format(path)
+            logging.error(msg)
+            return False, msg
         
-        # TODO: check if file is video file
-        # TODO: get width, heigh and frame rate using ffprobe
+        # Check if file is video file
+        mimetype = magic.from_file(path, mime=True)
+        if not mimetype.startswith('video/'):
+            msg = 'File "{:s}" does not appear to be a video file ({:s})'
+            msg = msg.format(path, mimetype)
+            logging.error(msg)
+            return False, msg
+
+        # Create a QueueItem instance and then probe the file to retrieve its
+        # attributes
+        item = QueueItem(path)
+        if not item.probe():
+            msg = 'An error ocurred while probing the video file'
+            logging.error(msg)
+            return False, msg
         
-        ENCODE_QUEUE.append(path)
+        # Add item to the queue
+        ENCODE_QUEUE.add(item)
         
         return True, 'Added to queue'
         
diff --git a/software/hbkcd/hbkcd/config.py b/software/hbkcd/hbkcd/config.py
new file mode 100644
index 0000000..bfcb5cc
--- /dev/null
+++ b/software/hbkcd/hbkcd/config.py
@@ -0,0 +1,23 @@
+import logging
+
+###############################################################################
+# 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'
+
+# Absolute path to the ffprobe executable
+FFPROBE_PATH = '/home/dsorber/ffprobe'
+
+# The ffprobe command used to get information about the video file
+FFPROBE_CMD = '{:s} -show_streams -show_entries stream=width,height,'\
+              'display_aspect_ratio,r_frame_rate,bit_rate,codec_type,'\
+              'codec_name -of json -v quiet -i "{:s}"'
+
+# Command server port
+COMMAND_SERVER_PORT = 8081
diff --git a/software/hbkcd/hbkcd/daemon.py b/software/hbkcd/hbkcd/daemon.py
index eb479a6..f15bf25 100755
--- a/software/hbkcd/hbkcd/daemon.py
+++ b/software/hbkcd/hbkcd/daemon.py
@@ -5,43 +5,39 @@ import sys
 import threading
 import time
 
+from hbkcd.config import *
 from hbkcd.command_server import StoppableHTTPServer, CommandHandler
-from hbkcd.queue import ENCODE_QUEUE
+# ~ 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 __test_executable(exe_name, exe_path):
+    """ Helper function to cut down on code duplication.
+    """
+    # Make sure specified exe path exists and is a file
+    if not os.path.isfile(exe_path):
+        logging.error('{:s} does not exist at path: {:s}'
+                      .format(exe_name, exe_path))
+        return False
+    
+    # Make sure the specified exe file is executable
+    if not os.access(exe_path, os.X_OK):
+        logging.error('{:s} exists but is not executable ({:s})'
+                      .format(exe_name, exe_path))
+        return False
+        
+    return True
 
 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
+    # Check for both the Handbrake and ffprobe executables
+    for exe in [('Handbrake', HANDBRAKE_PATH), ('ffprobe', FFPROBE_PATH)]:
+        if not __test_executable(exe[0], exe[1]):
+            return False
     
     logging.debug('Runtime configuration checks OKAY')
     
diff --git a/software/hbkcd/hbkcd/queue.py b/software/hbkcd/hbkcd/queue.py
index 92bbb97..01df141 100644
--- a/software/hbkcd/hbkcd/queue.py
+++ b/software/hbkcd/hbkcd/queue.py
@@ -1,7 +1,119 @@
+import datetime
+import json
+import os
+import subprocess
 
-ENCODE_QUEUE = []
+from hbkcd.config import *
+
+def format_ds(ds):
+    """ Helper function for formatting datetime stamps
+    """
+    if ds is None:
+        return ''
+    else:
+        return ds.strftime('%m/%d/%Y %H:%M:%S')
+        
+class EncodeQueue(object):
+    
+    def __init__(self):
+        self.queue = []
+        
+    def add(self, item):
+        # Add item then set position
+        logging.info('Adding: {:s} -- {}x{} at {} fps'.format(item.path, 
+                                                              item.width, 
+                                                              item.height, 
+                                                              item.framerate))
+        
+        self.queue.append(item)
+        position = len(self.queue)
+        item.position = position
+            
+    def remove(self, idx):
+        # TODO: implement removal
+        pass
+        
+    def get_list(self):
+        """ Get queue as a list of dicts that can be converted to JSON easily
+        """
+        return [item.get_dict_rep() for item in self.queue]
+        
 
 class QueueItem(object):
 
     def __init__(self, path):
         self.path = path
+        self.filename = os.path.basename(path)
+        self.position = None
+        self.in_progress = False
+        
+        # Video
+        self.width = None
+        self.height = None
+        self.framerate = None
+        
+        # Audio
+        self.convert_audio = False
+
+        self.time_added = datetime.datetime.now()
+        self.encode_started = None
+        self.encode_finished = None
+        
+        self.encode_done = None
+        
+    def probe(self):
+        # Fill in ffprobe command
+        cmd = FFPROBE_CMD.format(FFPROBE_PATH, self.path)
+        proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 
+                              stderr=subprocess.PIPE)
+        
+        probe_okay = True
+        if proc.returncode == 0:
+            # Parse the output
+            output = json.loads(proc.stdout)
+            
+            # TODO: consider the posibility of more than 2 streams 
+            #       (would handbrake handle this???)
+            for stream_info in output['streams']:
+                if stream_info['codec_type'] == 'video':
+                    # Grab video parameters
+                    self.width = stream_info['width']
+                    self.height = stream_info['height']
+                    
+                    # Frame rate is a tricky devil
+                    rate_ratio = stream_info['r_frame_rate'].split('/')
+                    framerate = float(rate_ratio[0]) / float(rate_ratio[1])
+                    framerate = round(framerate, 2)
+                    
+                    # Determine if there is a fractional part
+                    if (framerate - int(framerate)) == 0:
+                        self.framerate = str(int(framerate))
+                    else:
+                        self.framerate = str(framerate)
+                        
+                elif stream_info['codec_type'] == 'audio':
+                    # For now we're converting everything to AAC
+                    self.convert_audio = stream_info['codec_name'] != 'aac'
+            
+        else:
+            probe_okay = False
+            logging.error('ffprobe failed: {:s}'.format(
+                proc.stderr.decode('utf-8')))
+        
+        return probe_okay
+        
+    def get_dict_rep(self):
+        return {'position':         self.position,
+                'path':             self.path,
+                'in_progress':      self.in_progress,
+                'width':            self.width,
+                'height':           self.height,
+                'framerate':        self.framerate,
+                'convert_audio':    self.convert_audio,
+                'time_added':       format_ds(self.time_added),
+                'encode_started':   format_ds(self.encode_started),
+                'encode_finished':  format_ds(self.encode_finished)}
+
+
+# Does this need to be protected?
+ENCODE_QUEUE = EncodeQueue()
diff --git a/software/hbkcd/setup.py b/software/hbkcd/setup.py
index 7834cb4..94b7238 100755
--- a/software/hbkcd/setup.py
+++ b/software/hbkcd/setup.py
@@ -3,8 +3,10 @@ from setuptools import setup, find_packages
 
 setup(
     name = 'hbkcd',
-    version = '0.0.1',
+    version = '0.0.2',
     description = 'HandBrake Control Daemon',
     packages = find_packages(),
+    python_requires = '>=3.6.0',
+    zip_safe = True
 )
     
