]> code.delx.au - bluplayer/blob - bluplayer.py
Improved error handling and messages
[bluplayer] / bluplayer.py
1 #!/usr/bin/python
2
3 import csv
4 import errno
5 import logging
6 import os
7 import subprocess
8 import sys
9 import urllib
10
11 from lxml import etree
12 from PyQt4.QtCore import *
13 from PyQt4.QtGui import *
14
15
16 XHTML = "http://www.w3.org/1999/xhtml"
17 NS = {
18 "xhtml": XHTML,
19 }
20 DEFAULT_PATH = "/usr/local/bin:/usr/bin:/bin"
21
22
23 def set_env_config(name, value):
24 value = os.environ.get(name, value)
25 globals()[name] = value
26
27 def find_path(binary):
28 value = ""
29 paths = os.environ.get("PATH", DEFAULT_PATH).split(":")
30 for path in paths:
31 path = os.path.join(path, binary)
32 if os.path.isfile(path):
33 value = path
34 break
35 return value
36
37 set_env_config("MAKEMKVCON_PATH", find_path("makemkvcon"))
38 set_env_config("MPLAYER_PATH", find_path("mplayer"))
39 set_env_config("MPLAYER_OPTS", None)
40
41
42 def grab_xml(url):
43 f = urllib.urlopen(url)
44 doc = etree.parse(f)
45 f.close()
46 return doc
47
48 def parse_doc(doc):
49 d = {}
50 tds = doc.xpath("//xhtml:td", namespaces=NS)
51 i = 0
52 while i < len(tds)-1:
53 key = tds[i].text
54 value = tds[i+1]
55 for v in value:
56 if v.tag == "{%s}a" % XHTML:
57 value = v.get("href")
58 break
59 else:
60 value = value.text
61
62 d[key] = value
63 i += 2
64 return d
65
66 def get_num_cpus():
67 logging.info("Determing number of CPUs")
68 try:
69 return subprocess.check_output("nproc").strip()
70 except Exception, e:
71 logging.warn("Unable to run nproc: %s", fmt_exc(e))
72 return 1
73
74 def grab_dict(url):
75 doc = grab_xml(url)
76 d = parse_doc(doc)
77 return d
78
79 def fmt_exc(e):
80 return "%s: %s" % (e.__class__.__name__, str(e))
81
82
83 class Title(object):
84 def __init__(self, title_page):
85 self.id = title_page["id"]
86 self.duration = title_page["duration"]
87 self.video_url = title_page["file0"]
88
89 def __str__(self):
90 return "Title%s: %s %s" % (self.id, self.duration, self.video_url)
91
92
93 class MakeMkvCon(object):
94 def __init__(self, params):
95 cmd = [MAKEMKVCON_PATH, "--robot"]
96 cmd.extend(params)
97 logging.info("Running makemkvcon: %s", params)
98 self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
99 self.fd = self.p.stdout.fileno()
100 self.buf = ""
101 self.finished = False
102 self.status = None
103
104 def decode_line(self, line):
105 logging.debug("makemkvcon output: %s", line)
106 data = csv.reader([line]).next()
107 key, x0 = data[0].split(":", 1)
108 data[0] = x0
109 return key, data
110
111 def __iter__(self):
112 return self
113
114 def next(self):
115 if self.finished:
116 raise StopIteration()
117 while True:
118 pos = self.buf.find("\n")
119 if pos >= 0:
120 result = self.buf[:pos]
121 self.buf = self.buf[pos+1:]
122 return self.decode_line(result)
123
124 try:
125 data = os.read(self.p.stdout.fileno(), 4096)
126 except OSError, e:
127 if e.errno == errno.EINTR:
128 continue
129 if not data:
130 self.status = self.p.wait()
131 logging.info("makemkvcon exited with status %s", self.status)
132 self.finished = True
133 raise StopIteration()
134
135 self.buf += data
136
137
138 class MPlayer(QObject):
139 play_finished = pyqtSignal()
140 fatal_error = pyqtSignal(str, str)
141
142 def run(self):
143 logging.info("mplayer thread started")
144
145 if not os.path.isfile(MPLAYER_PATH):
146 self.fatal_error.emit(
147 "MPlayer was not found.",
148 "Please install MPlayer. If you already have done so you " +
149 "may set the MPLAYER_PATH environment variable to the " +
150 "absolute path to the mplayer executable."
151 )
152 return
153
154 if MPLAYER_OPTS:
155 self.opts = MPLAYER_OPTS.split()
156 else:
157 self.opts = [
158 "-fs",
159 "-lavdopts", "threads=%s" % get_num_cpus(),
160 "-volume", "100",
161 ]
162
163 def play(self, video_url):
164 video_url = str(video_url)
165 logging.info("Running mplayer: %s", video_url)
166 try:
167 subprocess.check_call([MPLAYER_PATH] + self.opts + [video_url])
168 except Exception, e:
169 self.fatal_error.emit("MPlayer failed to play the video.", fmt_exc(e))
170 finally:
171 self.play_finished.emit()
172
173
174 class MakeMkv(QObject):
175 title_loaded = pyqtSignal(Title)
176 title_load_complete = pyqtSignal()
177 status = pyqtSignal(str)
178 fatal_error = pyqtSignal(str, str)
179
180 def find_disc(self):
181 self.url = "http://192.168.1.114:51001/"
182 makemkvcon = MakeMkvCon(["info", "disc:9999"])
183 disc_number = None
184 for key, line in makemkvcon:
185 if key == "MSG" and line[0] != "5010":
186 self.status.emit(line[3])
187
188 if disc_number is None and key == "DRV" and line[5]:
189 disc_number = line[0]
190 disc_name = line[5]
191 self.status.emit("Found disc %s" % disc_name)
192
193 if makemkvcon.status == 0:
194 return disc_number
195
196 def run_stream(self, disc_number):
197 makemkvcon = MakeMkvCon(["stream", "disc:%s" % disc_number])
198 for key, line in makemkvcon:
199 if key == "MSG" and line[0] == "4500":
200 # Sometimes the port in field 6 is wrong
201 port = line[5].split(":")[1]
202 url = "http://localhost:%s/" % port
203 self.load_titles(url)
204 elif key == "MSG":
205 self.status.emit(line[3])
206
207 if makemkvcon.status != 0:
208 self.fatal_error.emit(
209 "MakeMKV quit unexpectedly.",
210 "makemkvcon exited with code %s" % makemkvcon.status
211 )
212
213 def load_titles(self, url):
214 home_page = grab_dict(url)
215 title_list_page = grab_dict(home_page["titles"])
216 title_count = int(title_list_page["titlecount"])
217 for i in xrange(title_count):
218 title_page = grab_dict(title_list_page["title%d" % i])
219 title = Title(title_page)
220 self.title_loaded.emit(title)
221 self.title_load_complete.emit()
222
223 def run(self):
224 logging.info("makemkv thread started")
225
226 if not os.path.isfile(MAKEMKVCON_PATH):
227 self.fatal_error.emit(
228 "MakeMKV was not found.",
229 "Please install MakeMKV. If you already have done so you " +
230 "may set the MAKEMKVCON_PATH environment variable to the " +
231 "absolute path to the makemkvcon executable."
232 )
233 return
234
235 try:
236 disc_number = self.find_disc()
237 except Exception, e:
238 self.fatal_error.emit("Error searching for disc.", fmt_exc(e))
239 raise
240
241 if not disc_number:
242 self.fatal_error.emit(
243 "No disc found.",
244 "Please insert a BluRay disc and try again."
245 )
246 return
247
248 try:
249 self.run_stream(disc_number)
250 except Exception, e:
251 self.fatal_error.emit("Failed to start MakeMKV.", fmt_exc(e))
252 raise
253
254 logging.info("makemkv thread finished")
255
256
257 class PlayerWindow(QWidget):
258 fatal_error = pyqtSignal(str, str)
259 video_selected = pyqtSignal(str)
260
261 def __init__(self):
262 QWidget.__init__(self)
263
264 self.title_map = {}
265 self.is_playing = False
266
267 self.list_widget = QListWidget(self)
268 self.list_widget.itemActivated.connect(self.handle_activated)
269 self.list_widget.setEnabled(False)
270
271 self.log = QTextEdit(self)
272 self.log.setReadOnly(True)
273
274 self.splitter = QSplitter(Qt.Vertical, self)
275 self.splitter.addWidget(self.list_widget)
276 self.splitter.addWidget(self.log)
277 self.splitter.setSizes([900, 200])
278
279 self.layout = QVBoxLayout(self)
280 self.layout.addWidget(self.splitter)
281 self.setWindowTitle("BluPlayer")
282 self.resize(1200, 700)
283
284 def add_title(self, title):
285 name = "Title %s (%s)" % (int(title.id)+1, title.duration)
286 self.list_widget.addItem(name)
287 self.title_map[name] = title
288
289 def select_longest_title(self):
290 longest_title = None
291 longest_item = None
292 for i in xrange(self.list_widget.count()):
293 item = self.list_widget.item(i)
294 name = str(item.text())
295 title = self.title_map[name]
296 if longest_title is None or title.duration > longest_title.duration:
297 longest_title = title
298 longest_item = item
299 self.list_widget.setCurrentItem(longest_item)
300 self.list_widget.setEnabled(True)
301 self.list_widget.setFocus()
302
303 def add_log_entry(self, text):
304 self.log.append(text)
305
306 def handle_activated(self, item):
307 if self.is_playing:
308 return
309 name = str(item.text())
310 title = self.title_map[name]
311 self.is_playing = True
312 self.list_widget.setEnabled(False)
313 self.video_selected.emit(title.video_url)
314
315 def set_play_finished(self):
316 self.is_playing = False
317 self.list_widget.setEnabled(True)
318 self.list_widget.setFocus()
319
320 def keyPressEvent(self, e):
321 if e.key() in (Qt.Key_Escape, Qt.Key_Backspace):
322 self.close()
323 return
324 QWidget.keyPressEvent(self, e)
325
326 class LoadingDialog(QProgressDialog):
327 def __init__(self, parent):
328 QProgressDialog.__init__(self, parent)
329 self.setWindowModality(Qt.WindowModal);
330 self.setWindowTitle("Loading disc")
331 self.setLabelText("Loading BluRay disc. Please wait...")
332 self.setCancelButtonText("Exit")
333 self.setMinimum(0)
334 self.setMaximum(0)
335
336 class ErrorDialog(QMessageBox):
337 fatal_error = pyqtSignal(str, str)
338
339 def __init__(self, parent):
340 QMessageBox.__init__(self, parent)
341 self.setStandardButtons(QMessageBox.Ok)
342 self.setDefaultButton(QMessageBox.Ok)
343 self.fatal_error.connect(self.configure_popup)
344 self.setWindowTitle("Fatal error")
345 self.has_run = False
346
347 def configure_popup(self, text, detail):
348 if self.has_run:
349 return
350 self.has_run = True
351 self.setText(text)
352 self.setInformativeText(detail)
353 QTimer.singleShot(0, self.show_and_exit)
354
355 def show_and_exit(self):
356 logging.info("showing and exiting")
357 self.exec_()
358 qApp.quit()
359
360 def killall_makemkvcon():
361 logging.info("Stopping any makemkvcon processes")
362 subprocess.Popen(["killall", "--quiet", "makemkvcon"]).wait()
363
364 def main():
365 logging.basicConfig(format="%(levelname)s: %(message)s")
366 logging.getLogger().setLevel(logging.DEBUG)
367 logging.info("Configuring application")
368
369 app = QApplication(sys.argv)
370 app.setQuitOnLastWindowClosed(True)
371
372 default_font = app.font()
373 default_font.setPointSize(16)
374 app.setFont(default_font)
375
376 player_window = PlayerWindow()
377 player_window.show()
378
379 loading_dialog = LoadingDialog(player_window)
380 loading_dialog.show()
381
382 error_dialog = ErrorDialog(player_window)
383
384 makemkv = MakeMkv()
385 makemkv_thread = QThread()
386 makemkv.moveToThread(makemkv_thread)
387 makemkv_thread.started.connect(makemkv.run)
388
389 mplayer = MPlayer()
390 mplayer_thread = QThread()
391 mplayer.moveToThread(mplayer_thread)
392 mplayer_thread.started.connect(mplayer.run)
393
394 makemkv.title_loaded.connect(player_window.add_title)
395 makemkv.title_load_complete.connect(player_window.select_longest_title)
396 makemkv.status.connect(player_window.add_log_entry)
397 makemkv.fatal_error.connect(error_dialog.fatal_error)
398 makemkv.title_load_complete.connect(loading_dialog.reset)
399
400 player_window.video_selected.connect(mplayer.play)
401 mplayer.play_finished.connect(player_window.set_play_finished)
402 mplayer.fatal_error.connect(error_dialog.fatal_error)
403
404 player_window.fatal_error.connect(error_dialog.fatal_error)
405 error_dialog.fatal_error.connect(loading_dialog.reset)
406 loading_dialog.canceled.connect(qApp.quit)
407
408 logging.info("Starting application")
409 makemkv_thread.start()
410 mplayer_thread.start()
411 result = app.exec_()
412
413 logging.info("Shutting down")
414 makemkv_thread.quit()
415 mplayer_thread.quit()
416 killall_makemkvcon()
417 logging.info("Waiting for makemkv thread")
418 makemkv_thread.wait(2000)
419 logging.info("Waiting for mplayer thread")
420 mplayer_thread.wait(2000)
421 logging.info("Exiting...")
422 sys.exit(result)
423
424 if __name__ == "__main__":
425 main()
426