Revision 3ce1c609
Added by David Sorber over 7 years ago
| software/hbkcd/hbkcd/command_server.py | ||
|---|---|---|
|
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
|
||
| ... | ... | |
|
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:
|
||
| ... | ... | |
|
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)
|
||
| ... | ... | |
|
# 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'
|
||
|
|
||
| software/hbkcd/hbkcd/config.py | ||
|---|---|---|
|
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
|
||
| software/hbkcd/hbkcd/daemon.py | ||
|---|---|---|
|
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')
|
||
|
|
||
| software/hbkcd/hbkcd/queue.py | ||
|---|---|---|
|
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()
|
||
| software/hbkcd/setup.py | ||
|---|---|---|
|
|
||
|
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
|
||
|
)
|
||
|
|
||
Greatly improved the queue.