]> code.delx.au - bluplayer/blob - bluplayer.py
Initial commit
[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
21 MAKEMKV_DIR = os.environ.get("MAKEMKV_DIR", os.path.expanduser("~/.makemkv_install"))
22 MAKEMKVCON_PATH = os.environ.get("MAKEMKVCON_PATH", os.path.join(MAKEMKV_DIR, "current", "makemkvcon"))
23 MPLAYER_PATH = os.environ.get("MPLAYER_PATH", "mplayer")
24
25
26 def grab_xml(url):
27 f = urllib.urlopen(url)
28 doc = etree.parse(f)
29 f.close()
30 return doc
31
32 def parse_doc(doc):
33 d = {}
34 tds = doc.xpath("//xhtml:td", namespaces=NS)
35 i = 0
36 while i < len(tds)-1:
37 key = tds[i].text
38 value = tds[i+1]
39 for v in value:
40 if v.tag == "{%s}a" % XHTML:
41 value = v.get("href")
42 break
43 else:
44 value = value.text
45
46 d[key] = value
47 i += 2
48 return d
49
50 def grab_dict(url):
51 doc = grab_xml(url)
52 d = parse_doc(doc)
53 return d
54
55 def format_exception(msg, e=None):
56 msg = "%s\n%s: %s" % (msg, e.__class__.__name__, str(e))
57 logging.error(msg)
58 return msg
59
60
61 class Title(object):
62 def __init__(self, title_page):
63 self.id = title_page["id"]
64 self.duration = title_page["duration"]
65 self.video_url = title_page["file0"]
66
67 def __str__(self):
68 return "Title%s: %s %s" % (self.id, self.duration, self.video_url)
69
70 def play(self):
71 cmd = [
72 MPLAYER_PATH,
73 "-fs",
74 "-lavdopts", "threads=%s" % subprocess.check_output("nproc").strip(),
75 "-volume", "100",
76 self.video_url,
77 ]
78 logging.info("Running mplayer: %s", self.video_url)
79 subprocess.check_call(cmd)
80
81
82 class MakeMkvCon(object):
83 def __init__(self, params):
84 cmd = [MAKEMKVCON_PATH, "--robot"]
85 cmd.extend(params)
86 logging.info("Running makemkvcon: %s", params)
87 self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
88 self.fd = self.p.stdout.fileno()
89 self.buf = ""
90 self.finished = False
91 self.status = None
92
93 def decode_line(self, line):
94 logging.debug("makemkvcon output: %s", line)
95 data = csv.reader([line]).next()
96 key, x0 = data[0].split(":", 1)
97 data[0] = x0
98 return key, data
99
100 def __iter__(self):
101 return self
102
103 def next(self):
104 if self.finished:
105 raise StopIteration()
106 while True:
107 pos = self.buf.find("\n")
108 if pos >= 0:
109 result = self.buf[:pos]
110 self.buf = self.buf[pos+1:]
111 return self.decode_line(result)
112
113 try:
114 data = os.read(self.p.stdout.fileno(), 4096)
115 except OSError, e:
116 if e.errno == errno.EINTR:
117 continue
118 if not data:
119 self.status = self.p.wait()
120 logging.info("makemkvcon exited with status %s", self.status)
121 self.finished = True
122 raise StopIteration()
123
124 self.buf += data
125
126
127 class MakeMkv(QObject):
128 title_loaded = pyqtSignal(Title)
129 status = pyqtSignal(str)
130 fatal_error = pyqtSignal(str)
131
132 def install(self):
133 raise NotImplementedError("auto-install not implemented")
134
135 def find_disc(self):
136 self.url = "http://192.168.1.114:51001/"
137 makemkvcon = MakeMkvCon(["info", "disc:9999"])
138 disc_number = None
139 for key, line in makemkvcon:
140 if key == "MSG" and line[0] != "5010":
141 self.status.emit(line[3])
142
143 if disc_number is None and key == "DRV" and line[5]:
144 disc_number = line[0]
145 disc_name = line[5]
146 self.status.emit("Found disc %s" % disc_name)
147
148 if makemkvcon.status == 0:
149 return disc_number
150
151 def run_stream(self, disc_number):
152 makemkvcon = MakeMkvCon(["stream", "disc:%s" % disc_number])
153 for key, line in makemkvcon:
154 if key == "MSG" and line[0] == "4500":
155 url = "http://localhost:%s/" % line[6]
156 self.load_titles(url)
157 elif key == "MSG":
158 self.status.emit(line[3])
159
160 if makemkvcon.status != 0:
161 self.fatal_error.emit("MakeMKV exited with error status: %s" % makemkvcon.status)
162
163 def load_titles(self, url):
164 home_page = grab_dict(url)
165 title_list_page = grab_dict(home_page["titles"])
166 title_count = int(title_list_page["titlecount"])
167 for i in xrange(title_count):
168 title_page = grab_dict(title_list_page["title%d" % i])
169 title = Title(title_page)
170 self.title_loaded.emit(title)
171
172 def run(self):
173 logging.info("MakeMKV thread started")
174
175 if not os.path.isfile(MAKEMKVCON_PATH):
176 try:
177 self.install()
178 except Exception, e:
179 self.fatal_error.emit(format_exception("Failed to install MakeMKV", e))
180 raise
181
182 try:
183 disc_number = self.find_disc()
184 except Exception, e:
185 self.fatal_error.emit(format_exception("Error searching for disc", e))
186 raise
187
188 if not disc_number:
189 self.fatal_error.emit("No disc found, please insert a disc and try again.")
190 return
191
192 try:
193 self.run_stream(disc_number)
194 except Exception, e:
195 self.fatal_error.emit(format_exception("Failed to start MakeMKV", e))
196 raise
197
198 logging.info("MakeMKV thread finished")
199
200
201 class PlayerWindow(QWidget):
202 def __init__(self):
203 QWidget.__init__(self)
204 self.title_map = {}
205
206 self.list_widget = QListWidget(self)
207 self.list_widget.itemActivated.connect(self.handle_activated)
208 self.list_widget.setFocus()
209
210 self.log = QTextEdit(self)
211 self.log.setReadOnly(True)
212
213 self.splitter = QSplitter(Qt.Vertical, self)
214 self.splitter.addWidget(self.list_widget)
215 self.splitter.addWidget(self.log)
216 self.splitter.setSizes([900, 200])
217
218 self.layout = QVBoxLayout(self)
219 self.layout.addWidget(self.splitter)
220 self.setWindowTitle("BluPlayer")
221
222 def add_title(self, title):
223 name = "Title %s (%s)" % (title.id, title.duration)
224 self.list_widget.addItem(name)
225 if not self.title_map:
226 # select the first item
227 self.list_widget.setCurrentItem(self.list_widget.item(0))
228 self.title_map[name] = title
229
230 def add_log_entry(self, text):
231 self.log.append(text)
232
233 def popup_fatal_error(self, text):
234 QMessageBox.critical(None, "Fatal error", text)
235 qApp.quit()
236
237 def handle_activated(self, item):
238 name = str(item.text())
239 title = self.title_map[name]
240 try:
241 title.play()
242 except Exception, e:
243 popup_error_exit("MPlayer failed to play the video", e)
244
245 def keyPressEvent(self, e):
246 if e.key() in (Qt.Key_Escape, Qt.Key_Backspace):
247 self.close()
248 return
249 QWidget.keyPressEvent(self, e)
250
251
252 def killall_makemkvcon():
253 logging.info("killing makemkvcon")
254 subprocess.Popen(["killall", "makemkvcon"]).wait()
255
256 def main():
257 logging.basicConfig(format="%(levelname)s: %(message)s")
258 logging.getLogger().setLevel(logging.DEBUG)
259
260 app = QApplication(sys.argv)
261 app.setQuitOnLastWindowClosed(True)
262
263 default_font = app.font()
264 default_font.setPointSize(16)
265 app.setFont(default_font)
266
267 player_window = PlayerWindow()
268 player_window.resize(1200, 700)
269 player_window.show()
270
271 makemkv = MakeMkv()
272 worker_thread = QThread()
273 makemkv.moveToThread(worker_thread)
274
275 worker_thread.started.connect(makemkv.run)
276 makemkv.title_loaded.connect(player_window.add_title)
277 makemkv.status.connect(player_window.add_log_entry)
278 makemkv.fatal_error.connect(player_window.popup_fatal_error)
279
280 logging.info("Starting worker thread")
281 worker_thread.start()
282 result = app.exec_()
283
284 logging.info("Shutting down")
285 worker_thread.quit()
286 killall_makemkvcon()
287 logging.info("Waiting for worker thread")
288 worker_thread.wait(2000)
289 logging.info("Exiting...")
290 sys.exit(result)
291
292 if __name__ == "__main__":
293 main()
294