#!/usr/bin/python2 # # Listener for queuing long-term tasks # # Sample /etc/homesvr/ripserver.conf: # # [listener] # port = 9080 # # copypath = /copy/ # rippath = /rip/ # # qsize = 100 # # copycmd = PATH_TO/homesvr/autorip/udev_cb.py # ripcmd = /usr/bin/HandBrakeCLI # # drive = /dev/sr0 # import sys import os.path import ConfigParser import BaseHTTPServer import urlparse import time import Queue import threading import traceback import subprocess import fcntl import CDROM import cgi import ezt CONFIG_FNAME = '/etc/homesvr/ripserver.conf' SECTION_NAME = 'listener' # Queue operations Q_STOP = 'Q_STOP' # args: None Q_COPY = 'Q_COPY' # args: Q_RIP = 'Q_RIP' # args: # Response page templates COPY_REPLY = 'copyreply.ezt' RIP_REPLY = 'ripreply.ezt' # The ripping process should take at least this long, and produce a file of # at least this size. If not, then something went wrong. MIN_DURATION = 120 # two minutes MIN_SIZE = 1000000 # one megabyte # For locating the templates. THIS_DIR = os.path.dirname(os.path.realpath(__file__)) def run_listener(cfg): server = RipListener(cfg) try: # Assume that a HUP or something will end this loop. server.serve_forever() except KeyboardInterrupt: # Just eat this exception. pass finally: server.stop_worker() raise class RipListener(BaseHTTPServer.HTTPServer): def __init__(self, cfg): port = cfg.getint(SECTION_NAME, 'port') BaseHTTPServer.HTTPServer.__init__(self, ('', port), RipHandler) self.copypath = cfg.get(SECTION_NAME, 'copypath') # copy disk in drive self.rippath = cfg.get(SECTION_NAME, 'rippath') # rip a DVD to mp4 self.queue = Queue.Queue(cfg.getint(SECTION_NAME, 'qsize')) self.worker = threading.Thread(target=self.process_jobs) self.worker.start() self.copycmd = cfg.get(SECTION_NAME, 'copycmd') self.ripcmd = cfg.get(SECTION_NAME, 'ripcmd') self.drive = cfg.get(SECTION_NAME, 'drive') self.base_dir = cfg.get(SECTION_NAME, 'base_dir') self.T_copyreply = ezt.Template(os.path.join(THIS_DIR, COPY_REPLY), base_format=ezt.FORMAT_HTML) #self.T_ripreply = ezt.Template(os.path.join(THIS_DIR, RIP_REPLY), # base_format=ezt.FORMAT_HTML) _log_message('Listening on port: %d', port) def process_jobs(self): _log_message('WORKER: started.') try: while True: # Note: this blocks until a work item arrives. op, args = self.queue.get() if op is Q_STOP: break if op is Q_COPY: perform_copy(self.copycmd) elif op is Q_RIP: inputdir, outputdir = args perform_rip(self.ripcmd, inputdir, outputdir) else: _log_message('Unknown operation: %s <%s>', op, args) except: e, v, tb = sys.exc_info() lines = ''.join(traceback.format_exception(e, v, tb, limit=1)) _log_message('WORKER: Exception: %s', repr(lines)) def stop_worker(self): # Tell the thread to stop, once all jobs are complete. _log_message('WORKER: stopping...') self.queue.put((Q_STOP, None)) self.worker.join() _log_message('WORKER: stopped.') # similar to BaseHTTPRequestHandler.log_message() def _log_message(format, *args): sys.stderr.write("- - - [%s] %s\n" % (_log_date_time_string(), format % args)) # similar to BaseHTTPRequestHandler.log_date_time_string() def _log_date_time_string(): year, month, day, hh, mm, ss, _, _, _ = time.localtime() monthstr = BaseHTTPServer.BaseHTTPRequestHandler.monthname[month] return "%02d/%3s/%04d %02d:%02d:%02d" % (day, monthstr, year, hh, mm, ss) class RipHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_POST(self): parse = urlparse.urlparse(self.path) if parse.path == self.server.copypath: # PATH?noreply will return a 204. Otherwise, a nice response page. self.copy_disk(parse.query == 'noreply') elif parse.path == self.server.rippath: self.rip_dvd() else: self.send_error(404) # All branches above generated a response. Always close the connection. self.close_connection = 1 return def copy_disk(self, noreply): if disk_in_drive(self.server.drive): ### check for a full queue self.server.queue.put((Q_COPY, None)) self.log_message('COPY request queued.') if noreply: self.send_response(204) else: data = { 'count': self.server.queue.qsize(), # close enough } self.send_page(self.server.T_copyreply, data) else: self.send_error(400, 'COPY skipped. No disk in drive.') def rip_dvd(self): ### check for a full queue env = {'REQUEST_METHOD': 'POST', } form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env) inputdir = form.getfirst('idir') outputdir = form.getfirst('odir') if not inputdir or not outputdir: self.send_error(400, 'missing parameters') return inputdir = os.path.join(self.server.base_dir, inputdir) outputdir = os.path.join(self.server.base_dir, outputdir) if not os.path.exists(inputdir): self.send_error(400, 'dir does not exist: %s' % (inputdir,)) return if not os.path.exists(outputdir): self.send_error(400, 'dir does not exist: %s' % (outputdir,)) return self.server.queue.put((Q_RIP, (inputdir, outputdir))) self.send_error(204, 'RIP request queued.') # not actually an error def send_page(self, template, data): self.send_response(200) self.end_headers() template.generate(self.wfile, data) def perform_copy(copycmd): _log_message('COPY START') t = time.time() null = open('/dev/null', 'w') rc = subprocess.call([copycmd], stdout=null, stderr=null) duration = (time.time() - t) / 60 if rc == 0: _log_message('COPY DONE: duration: %.1f minutes', duration) else: _log_message('COPY ERROR: rc=%d ; duration: %.1f minutes', rc, duration) def perform_rip(ripcmd, inputdir, outputdir, min_duration=MIN_DURATION, min_size=MIN_SIZE): """Perform a DVD rip, creating a .mp4 for the main title. ripcmd: full path to the program to perform the rip (eg. HandBrakeCLI) inputdir: where the DVD content is located (eg. /dev/sr0 or .../VIDEO_TS) outputdir: where to place the resulting .mp4 """ _log_message('RIP START:') ### more args t = time.time() outputdir = os.path.realpath(outputdir) dirname = os.path.basename(outputdir) # .mp4 should match the containing directory output = os.path.join(outputdir, dirname + '.mp4') cmdline = (ripcmd, '-i', inputdir, '-o', output, '--main-feature', '-N', 'eng', '--native-dub', '--preset', 'HQ 480p30 Surround', # DVD; no need to go above 480 '--markers', ) _log_message('RIP CMD: %s', cmdline) null = open('/dev/null', 'w') # Discard output/errors. We'll call it "success" based on the duration, # and the size of the file. rc = subprocess.call(cmdline, stdout=null, stderr=null) if rc == 0: duration = time.time() - t size = os.stat(output).st_size if duration < min_duration or size < min_size: _log_message('RIP: duration=%.1f seconds, size=%d. Not good. Retrying with --no-dvdnav', duration, size) cmdline += ('--no-dvdnav', ) rc = subprocess.call(cmdline, stdout=null, stderr=null) if rc != 0: _log_message('RIP: failure. rc=%d', rc) ### if too fast or too small, then try this: _log_message('RIP DONE: duration: %.1f minutes', (time.time() - t) / 60) def disk_in_drive(drive): fd = os.open(drive, os.O_RDONLY | os.O_NONBLOCK) rv = fcntl.ioctl(fd, CDROM.CDROM_DRIVE_STATUS) os.close(fd) return rv == CDROM.CDS_DISC_OK def main(): cfg = ConfigParser.SafeConfigParser() cfg.read(CONFIG_FNAME) run_listener(cfg) if __name__ == '__main__': main()