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