+#!/usr/bin/python
+
+import csv
+import errno
+import logging
+import os
+import subprocess
+import sys
+import urllib
+
+from lxml import etree
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+
+
+XHTML = "http://www.w3.org/1999/xhtml"
+NS = {
+ "xhtml": XHTML,
+}
+
+MAKEMKV_DIR = os.environ.get("MAKEMKV_DIR", os.path.expanduser("~/.makemkv_install"))
+MAKEMKVCON_PATH = os.environ.get("MAKEMKVCON_PATH", os.path.join(MAKEMKV_DIR, "current", "makemkvcon"))
+MPLAYER_PATH = os.environ.get("MPLAYER_PATH", "mplayer")
+
+
+def grab_xml(url):
+ f = urllib.urlopen(url)
+ doc = etree.parse(f)
+ f.close()
+ return doc
+
+def parse_doc(doc):
+ d = {}
+ tds = doc.xpath("//xhtml:td", namespaces=NS)
+ i = 0
+ while i < len(tds)-1:
+ key = tds[i].text
+ value = tds[i+1]
+ for v in value:
+ if v.tag == "{%s}a" % XHTML:
+ value = v.get("href")
+ break
+ else:
+ value = value.text
+
+ d[key] = value
+ i += 2
+ return d
+
+def grab_dict(url):
+ doc = grab_xml(url)
+ d = parse_doc(doc)
+ return d
+
+def format_exception(msg, e=None):
+ msg = "%s\n%s: %s" % (msg, e.__class__.__name__, str(e))
+ logging.error(msg)
+ return msg
+
+
+class Title(object):
+ def __init__(self, title_page):
+ self.id = title_page["id"]
+ self.duration = title_page["duration"]
+ self.video_url = title_page["file0"]
+
+ def __str__(self):
+ return "Title%s: %s %s" % (self.id, self.duration, self.video_url)
+
+ def play(self):
+ cmd = [
+ MPLAYER_PATH,
+ "-fs",
+ "-lavdopts", "threads=%s" % subprocess.check_output("nproc").strip(),
+ "-volume", "100",
+ self.video_url,
+ ]
+ logging.info("Running mplayer: %s", self.video_url)
+ subprocess.check_call(cmd)
+
+
+class MakeMkvCon(object):
+ def __init__(self, params):
+ cmd = [MAKEMKVCON_PATH, "--robot"]
+ cmd.extend(params)
+ logging.info("Running makemkvcon: %s", params)
+ self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ self.fd = self.p.stdout.fileno()
+ self.buf = ""
+ self.finished = False
+ self.status = None
+
+ def decode_line(self, line):
+ logging.debug("makemkvcon output: %s", line)
+ data = csv.reader([line]).next()
+ key, x0 = data[0].split(":", 1)
+ data[0] = x0
+ return key, data
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if self.finished:
+ raise StopIteration()
+ while True:
+ pos = self.buf.find("\n")
+ if pos >= 0:
+ result = self.buf[:pos]
+ self.buf = self.buf[pos+1:]
+ return self.decode_line(result)
+
+ try:
+ data = os.read(self.p.stdout.fileno(), 4096)
+ except OSError, e:
+ if e.errno == errno.EINTR:
+ continue
+ if not data:
+ self.status = self.p.wait()
+ logging.info("makemkvcon exited with status %s", self.status)
+ self.finished = True
+ raise StopIteration()
+
+ self.buf += data
+
+
+class MakeMkv(QObject):
+ title_loaded = pyqtSignal(Title)
+ status = pyqtSignal(str)
+ fatal_error = pyqtSignal(str)
+
+ def install(self):
+ raise NotImplementedError("auto-install not implemented")
+
+ def find_disc(self):
+ self.url = "http://192.168.1.114:51001/"
+ makemkvcon = MakeMkvCon(["info", "disc:9999"])
+ disc_number = None
+ for key, line in makemkvcon:
+ if key == "MSG" and line[0] != "5010":
+ self.status.emit(line[3])
+
+ if disc_number is None and key == "DRV" and line[5]:
+ disc_number = line[0]
+ disc_name = line[5]
+ self.status.emit("Found disc %s" % disc_name)
+
+ if makemkvcon.status == 0:
+ return disc_number
+
+ def run_stream(self, disc_number):
+ makemkvcon = MakeMkvCon(["stream", "disc:%s" % disc_number])
+ for key, line in makemkvcon:
+ if key == "MSG" and line[0] == "4500":
+ url = "http://localhost:%s/" % line[6]
+ self.load_titles(url)
+ elif key == "MSG":
+ self.status.emit(line[3])
+
+ if makemkvcon.status != 0:
+ self.fatal_error.emit("MakeMKV exited with error status: %s" % makemkvcon.status)
+
+ def load_titles(self, url):
+ home_page = grab_dict(url)
+ title_list_page = grab_dict(home_page["titles"])
+ title_count = int(title_list_page["titlecount"])
+ for i in xrange(title_count):
+ title_page = grab_dict(title_list_page["title%d" % i])
+ title = Title(title_page)
+ self.title_loaded.emit(title)
+
+ def run(self):
+ logging.info("MakeMKV thread started")
+
+ if not os.path.isfile(MAKEMKVCON_PATH):
+ try:
+ self.install()
+ except Exception, e:
+ self.fatal_error.emit(format_exception("Failed to install MakeMKV", e))
+ raise
+
+ try:
+ disc_number = self.find_disc()
+ except Exception, e:
+ self.fatal_error.emit(format_exception("Error searching for disc", e))
+ raise
+
+ if not disc_number:
+ self.fatal_error.emit("No disc found, please insert a disc and try again.")
+ return
+
+ try:
+ self.run_stream(disc_number)
+ except Exception, e:
+ self.fatal_error.emit(format_exception("Failed to start MakeMKV", e))
+ raise
+
+ logging.info("MakeMKV thread finished")
+
+
+class PlayerWindow(QWidget):
+ def __init__(self):
+ QWidget.__init__(self)
+ self.title_map = {}
+
+ self.list_widget = QListWidget(self)
+ self.list_widget.itemActivated.connect(self.handle_activated)
+ self.list_widget.setFocus()
+
+ self.log = QTextEdit(self)
+ self.log.setReadOnly(True)
+
+ self.splitter = QSplitter(Qt.Vertical, self)
+ self.splitter.addWidget(self.list_widget)
+ self.splitter.addWidget(self.log)
+ self.splitter.setSizes([900, 200])
+
+ self.layout = QVBoxLayout(self)
+ self.layout.addWidget(self.splitter)
+ self.setWindowTitle("BluPlayer")
+
+ def add_title(self, title):
+ name = "Title %s (%s)" % (title.id, title.duration)
+ self.list_widget.addItem(name)
+ if not self.title_map:
+ # select the first item
+ self.list_widget.setCurrentItem(self.list_widget.item(0))
+ self.title_map[name] = title
+
+ def add_log_entry(self, text):
+ self.log.append(text)
+
+ def popup_fatal_error(self, text):
+ QMessageBox.critical(None, "Fatal error", text)
+ qApp.quit()
+
+ def handle_activated(self, item):
+ name = str(item.text())
+ title = self.title_map[name]
+ try:
+ title.play()
+ except Exception, e:
+ popup_error_exit("MPlayer failed to play the video", e)
+
+ def keyPressEvent(self, e):
+ if e.key() in (Qt.Key_Escape, Qt.Key_Backspace):
+ self.close()
+ return
+ QWidget.keyPressEvent(self, e)
+
+
+def killall_makemkvcon():
+ logging.info("killing makemkvcon")
+ subprocess.Popen(["killall", "makemkvcon"]).wait()
+
+def main():
+ logging.basicConfig(format="%(levelname)s: %(message)s")
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ app = QApplication(sys.argv)
+ app.setQuitOnLastWindowClosed(True)
+
+ default_font = app.font()
+ default_font.setPointSize(16)
+ app.setFont(default_font)
+
+ player_window = PlayerWindow()
+ player_window.resize(1200, 700)
+ player_window.show()
+
+ makemkv = MakeMkv()
+ worker_thread = QThread()
+ makemkv.moveToThread(worker_thread)
+
+ worker_thread.started.connect(makemkv.run)
+ makemkv.title_loaded.connect(player_window.add_title)
+ makemkv.status.connect(player_window.add_log_entry)
+ makemkv.fatal_error.connect(player_window.popup_fatal_error)
+
+ logging.info("Starting worker thread")
+ worker_thread.start()
+ result = app.exec_()
+
+ logging.info("Shutting down")
+ worker_thread.quit()
+ killall_makemkvcon()
+ logging.info("Waiting for worker thread")
+ worker_thread.wait(2000)
+ logging.info("Exiting...")
+ sys.exit(result)
+
+if __name__ == "__main__":
+ main()
+