|
#!/usr/bin/python3
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
import logging
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import threading
|
|
|
|
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
|
|
|
|
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.get_list()}
|
|
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 potentially 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]
|
|
elif content['type'] == 'remove':
|
|
response = self.handle_remove_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
|
|
logging.error(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 or 'output' 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):
|
|
msg = 'File does not exist: {:s}'.format(path)
|
|
logging.error(msg)
|
|
return False, msg
|
|
|
|
# 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
|
|
|
|
# Check output default assume that the path is a direct file name
|
|
output_path = command_data['output']
|
|
if output_path[-4:].lower() not in ['.mkv', '.mp4']:
|
|
# Assume output directory, name output based on input filename
|
|
if not os.path.isdir(output_path):
|
|
try:
|
|
os.makedirs(output_path, exist_ok=True)
|
|
except PermissionError:
|
|
msg = 'Unable to create output directory: {:s}'
|
|
msg = msg.format(output_path)
|
|
logging.error(msg)
|
|
return False, msg
|
|
|
|
# Always default to Matroska output
|
|
filename = os.path.basename(path)
|
|
output_filename = '{:s}.mkv'.format(os.path.splitext(filename)[0])
|
|
|
|
# Build final output path
|
|
output_path = os.path.join(output_path, output_filename)
|
|
|
|
# Create a QueueItem instance and then probe the file to retrieve its
|
|
# attributes
|
|
item = QueueItem(path, output_path)
|
|
if not item.probe():
|
|
msg = 'An error ocurred while probing the video file'
|
|
logging.error(msg)
|
|
return False, msg
|
|
|
|
# Add item to the queue
|
|
ENCODE_QUEUE.add(item)
|
|
|
|
return True, 'Added to queue'
|
|
|
|
def handle_remove_cmd(self, command_data):
|
|
""" Handler for the "remove" command
|
|
"""
|
|
if 'position' not in command_data:
|
|
return False, 'Command format invalid'
|
|
|
|
position = int(command_data['position'])
|
|
|
|
success = ENCODE_QUEUE.remove(position - 1)
|
|
msg = 'Removed element {:d}'.format(position)
|
|
if not success:
|
|
msg = 'Failed to remove element {:d}'.format(position)
|
|
|
|
return success, msg
|
|
|
|
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())
|