NS = {
"xhtml": XHTML,
}
+DEFAULT_PATH = "/usr/local/bin:/usr/bin:/bin"
-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 set_env_config(name, value):
+ value = os.environ.get(name, value)
+ globals()[name] = value
+
+def find_path(binary):
+ value = ""
+ paths = os.environ.get("PATH", DEFAULT_PATH).split(":")
+ for path in paths:
+ path = os.path.join(path, binary)
+ if os.path.isfile(path):
+ value = path
+ break
+ return value
+
+set_env_config("MAKEMKVCON_PATH", find_path("makemkvcon"))
+set_env_config("PLAYER_NAME", "vlc")
+set_env_config("PLAYER_PATH", find_path(PLAYER_NAME))
+set_env_config("PLAYER_OPTS", None)
def grab_xml(url):
i += 2
return d
+def get_num_cpus():
+ logging.info("Determing number of CPUs")
+ try:
+ return subprocess.check_output("nproc").strip()
+ except Exception, e:
+ logging.warn("Unable to run nproc: %s", fmt_exc(e))
+ return 1
+
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
+def fmt_exc(e):
+ return "%s: %s" % (e.__class__.__name__, str(e))
class Title(object):
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):
self.buf += data
+class Player(QObject):
+ play_finished = pyqtSignal()
+ fatal_error = pyqtSignal(str, str)
+
+ def run(self):
+ logging.info("player thread started")
+
+ if not os.path.isfile(PLAYER_PATH):
+ self.fatal_error.emit(
+ PLAYER_NAME + " was not found.",
+ "Please install it. If you already have done so you " +
+ "may set the PLAYER_PATH environment variable to the " +
+ "absolute path to the player executable."
+ )
+ return
+
+ if PLAYER_OPTS:
+ self.opts = PLAYER_OPTS.split()
+ else:
+ self.opts = [
+ "--fullscreen",
+ ]
+
+ def play(self, video_url):
+ video_url = str(video_url)
+ logging.info("Running player: %s", video_url)
+ try:
+ subprocess.check_call([PLAYER_PATH] + self.opts + [video_url])
+ except Exception, e:
+ self.fatal_error.emit("Failed to play the video.", fmt_exc(e))
+ finally:
+ self.play_finished.emit()
+
+
class MakeMkv(QObject):
title_loaded = pyqtSignal(Title)
+ title_load_complete = pyqtSignal()
status = pyqtSignal(str)
- fatal_error = pyqtSignal(str)
-
- def install(self):
- raise NotImplementedError("auto-install not implemented")
+ fatal_error = pyqtSignal(str, str)
def find_disc(self):
self.url = "http://192.168.1.114:51001/"
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]
+ # Sometimes the port in field 6 is wrong
+ port = line[5].split(":")[1]
+ url = "http://localhost:%s/" % port
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)
+ self.fatal_error.emit(
+ "MakeMKV quit unexpectedly.",
+ "makemkvcon exited with code %s" % makemkvcon.status
+ )
def load_titles(self, url):
home_page = grab_dict(url)
title_page = grab_dict(title_list_page["title%d" % i])
title = Title(title_page)
self.title_loaded.emit(title)
+ self.title_load_complete.emit()
def run(self):
- logging.info("MakeMKV thread started")
+ 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
+ self.fatal_error.emit(
+ "MakeMKV was not found.",
+ "Please install MakeMKV. If you already have done so you " +
+ "may set the MAKEMKVCON_PATH environment variable to the " +
+ "absolute path to the makemkvcon executable."
+ )
+ return
try:
disc_number = self.find_disc()
except Exception, e:
- self.fatal_error.emit(format_exception("Error searching for disc", e))
+ self.fatal_error.emit("Error searching for disc.", fmt_exc(e))
raise
if not disc_number:
- self.fatal_error.emit("No disc found, please insert a disc and try again.")
+ self.fatal_error.emit(
+ "No disc found.",
+ "Please insert a BluRay 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))
+ self.fatal_error.emit("Failed to start MakeMKV.", fmt_exc(e))
raise
- logging.info("MakeMKV thread finished")
+ logging.info("makemkv thread finished")
class PlayerWindow(QWidget):
+ fatal_error = pyqtSignal(str, str)
+ video_selected = pyqtSignal(str)
+
def __init__(self):
QWidget.__init__(self)
+
self.title_map = {}
+ self.is_playing = False
self.list_widget = QListWidget(self)
self.list_widget.itemActivated.connect(self.handle_activated)
- self.list_widget.setFocus()
+ self.list_widget.setEnabled(False)
self.log = QTextEdit(self)
self.log.setReadOnly(True)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.splitter)
self.setWindowTitle("BluPlayer")
+ self.resize(1200, 700)
def add_title(self, title):
- name = "Title %s (%s)" % (title.id, title.duration)
+ name = "Title %s (%s)" % (int(title.id)+1, 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 select_longest_title(self):
+ longest_title = None
+ longest_item = None
+ for i in xrange(self.list_widget.count()):
+ item = self.list_widget.item(i)
+ name = str(item.text())
+ title = self.title_map[name]
+ if longest_title is None or title.duration > longest_title.duration:
+ longest_title = title
+ longest_item = item
+ self.list_widget.setCurrentItem(longest_item)
+ self.list_widget.setEnabled(True)
+ self.list_widget.setFocus()
+
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):
+ if self.is_playing:
+ return
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)
+ self.is_playing = True
+ self.list_widget.setEnabled(False)
+ self.video_selected.emit(title.video_url)
+
+ def set_play_finished(self):
+ self.is_playing = False
+ self.list_widget.setEnabled(True)
+ self.list_widget.setFocus()
def keyPressEvent(self, e):
if e.key() in (Qt.Key_Escape, Qt.Key_Backspace):
return
QWidget.keyPressEvent(self, e)
+class LoadingDialog(QProgressDialog):
+ def __init__(self, parent):
+ QProgressDialog.__init__(self, parent)
+ self.setWindowModality(Qt.WindowModal);
+ self.setWindowTitle("Loading disc")
+ self.setLabelText("Loading BluRay disc. Please wait...")
+ self.setCancelButtonText("Exit")
+ self.setMinimum(0)
+ self.setMaximum(0)
+
+class ErrorDialog(QMessageBox):
+ fatal_error = pyqtSignal(str, str)
+
+ def __init__(self, parent):
+ QMessageBox.__init__(self, parent)
+ self.setStandardButtons(QMessageBox.Ok)
+ self.setDefaultButton(QMessageBox.Ok)
+ self.fatal_error.connect(self.configure_popup)
+ self.setWindowTitle("Fatal error")
+ self.has_run = False
+
+ def configure_popup(self, text, detail):
+ if self.has_run:
+ return
+ self.has_run = True
+ self.setText(text)
+ self.setInformativeText(detail)
+ QTimer.singleShot(0, self.show_and_exit)
+
+ def show_and_exit(self):
+ logging.info("showing and exiting")
+ self.exec_()
+ qApp.quit()
def killall_makemkvcon():
- logging.info("killing makemkvcon")
- subprocess.Popen(["killall", "makemkvcon"]).wait()
+ logging.info("Stopping any makemkvcon processes")
+ subprocess.Popen(["killall", "--quiet", "makemkvcon"]).wait()
def main():
logging.basicConfig(format="%(levelname)s: %(message)s")
logging.getLogger().setLevel(logging.DEBUG)
+ logging.info("Configuring application")
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(True)
app.setFont(default_font)
player_window = PlayerWindow()
- player_window.resize(1200, 700)
player_window.show()
+ loading_dialog = LoadingDialog(player_window)
+ loading_dialog.show()
+
+ error_dialog = ErrorDialog(player_window)
+
makemkv = MakeMkv()
- worker_thread = QThread()
- makemkv.moveToThread(worker_thread)
+ makemkv_thread = QThread()
+ makemkv.moveToThread(makemkv_thread)
+ makemkv_thread.started.connect(makemkv.run)
+
+ player = Player()
+ player_thread = QThread()
+ player.moveToThread(player_thread)
+ player_thread.started.connect(player.run)
- worker_thread.started.connect(makemkv.run)
makemkv.title_loaded.connect(player_window.add_title)
+ makemkv.title_load_complete.connect(player_window.select_longest_title)
makemkv.status.connect(player_window.add_log_entry)
- makemkv.fatal_error.connect(player_window.popup_fatal_error)
+ makemkv.fatal_error.connect(error_dialog.fatal_error)
+ makemkv.title_load_complete.connect(loading_dialog.reset)
+
+ player_window.video_selected.connect(player.play)
+ player.play_finished.connect(player_window.set_play_finished)
+ player.fatal_error.connect(error_dialog.fatal_error)
+
+ player_window.fatal_error.connect(error_dialog.fatal_error)
+ error_dialog.fatal_error.connect(loading_dialog.reset)
+ loading_dialog.canceled.connect(qApp.quit)
- logging.info("Starting worker thread")
- worker_thread.start()
+ logging.info("Starting application")
+ makemkv_thread.start()
+ player_thread.start()
result = app.exec_()
logging.info("Shutting down")
- worker_thread.quit()
+ makemkv_thread.quit()
+ player_thread.quit()
killall_makemkvcon()
- logging.info("Waiting for worker thread")
- worker_thread.wait(2000)
+ logging.info("Waiting for makemkv thread")
+ makemkv_thread.wait(2000)
+ logging.info("Waiting for player thread")
+ player_thread.wait(2000)
logging.info("Exiting...")
sys.exit(result)