From: James Bunton Date: Fri, 15 Feb 2013 14:55:36 +0000 (+1100) Subject: Initial commit X-Git-Url: https://code.delx.au/bluplayer/commitdiff_plain/fad78222e09dfd589836dcffff7d30a26d07bfee Initial commit --- fad78222e09dfd589836dcffff7d30a26d07bfee diff --git a/bluplayer.py b/bluplayer.py new file mode 100755 index 0000000..ab10d99 --- /dev/null +++ b/bluplayer.py @@ -0,0 +1,294 @@ +#!/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() + diff --git a/makemkvcon_docs.txt b/makemkvcon_docs.txt new file mode 100644 index 0000000..c952a43 --- /dev/null +++ b/makemkvcon_docs.txt @@ -0,0 +1,113 @@ +makemkvcon [options] Command Parameters + +General options: + +--messages=file +Output all messages to file. Following special file names are recognized: +-stdout - stdout +-stderr - stderr +-null - disable output +Default is stdout + +--progress=file +Output all progress messages to file. The same special file names as in --messages are recognized with additional value "-same" to output to the same file as messages. Naturally --progress should follow --messages in this case. Default is no output. + +--debug[=file] +Enables debug messages and optionally changes the location of debug file. Default: program preferences. + +--directio=true/false +Enables or disables direct disc access. Default: program preferences. + +--noscan +Don't access any media during disc scan and do not check for media insertion and removal. Helpful when other applications already accessing discs in other drives. + +--cache=size +Specifies size of read cache in megabytes used by MakeMKV. By default program uses huge amount of memory. About 128 MB is recommended for streaming and backup, 512MB for DVD conversion and 1024MB for Blu-ray conversion. + +Streaming options: + +--upnp=true/false +Enable or disable UPNP streaming. Default: program preferences. + +--bindip=address string +Specify IP address to bind. Default: None, UPNP server binds to the first available address and web server listens on all available addresses. + +--bindport=port +Specify web server port to bind. Default: 51000. + +Backup options: + +--decrypt +Decrypt stream files during backup. Default: no decryption. + +Conversion options: + +--minlength=seconds +Specify minimum title length. Default: program preferences. + +Automation options. + +-r , --robot +Enables automation mode. Program will output more information in a format that is easier to parse. All output is line-based and output is flushed on line end. All strings are quoted, all control characters and quotes are backlash-escaped. If you automate this program it is highly recommended to use this option. Some options make reference to apdefs.h file that can be found in MakeMKV open-source package, included with version for Linux. These values will not change in future versions. + + +Message formats: + +Message output +MSG:code,flags,count,message,format,param0,param1,... +code - unique message code, should be used to identify particular string in language-neutral way. +flags - message flags, see AP_UIMSG_xxx flags in apdefs.h +count - number of parameters +message - raw message string suitable for output +format - format string used for message. This string is localized and subject to change, unlike message code. +paramX - parameter for message + +Current and total progress title +PRGC:code,id,name +PRGT:code,id,name +code - unique message code +id - operation sub-id +name - name string + +Progress bar values for current and total progress +PRGV:current,total,max +current - current progress value +total - total progress value +max - maximum possible value for a progress bar, constant + +Drive scan messages +DRV:index,visible,enabled,flags,drive name,disc name +index - drive index +visible - set to 1 if drive is present +enabled - set to 1 if drive is accessible +flags - media flags, see AP_DskFsFlagXXX in apdefs.h +drive name - drive name string +disc name - disc name string + +Disc information output messages +TCOUT:count +count - titles count + +Disc, title and stream information +CINFO:id,code,value +TINFO:id,code,value +SINFO:id,code,value + +id - attribute id, see AP_ItemAttributeId in apdefs.h +code - message code if attribute value is a constant string +value - attribute value + + +Examples: + +Copy all titles from first disc and save as MKV files: +makemkvcon mkv disc:0 all c:\folder + +List all available drives +makemkvcon -r --cache=1 info disc:9999 + +Backup first disc decrypting all video files in automation mode with progress output +makemkvcon backup --decrypt --cache=16 --noscan -r --progress=-same disc:0 c:\folder + +Start streaming server with all output suppressed on a specific address and port +makemvcon stream --upnp=1 --cache=128 --bindip=192.168.1.102 --bindport=51000 --messages=-none