9259b83825ddc8beea21a78b0ac4f681361c4894
[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 # Sometimes the port in field 6 is wrong
156 port = line[5].split(":")[1]
157 url = "http://localhost:%s/" % port
158 self.load_titles(url)
159 elif key == "MSG":
160 self.status.emit(line[3])
161
162 if makemkvcon.status != 0:
163 self.fatal_error.emit("MakeMKV exited with error status: %s" % makemkvcon.status)
164
165 def load_titles(self, url):
166 home_page = grab_dict(url)
167 title_list_page = grab_dict(home_page["titles"])
168 title_count = int(title_list_page["titlecount"])
169 for i in xrange(title_count):
170 title_page = grab_dict(title_list_page["title%d" % i])
171 title = Title(title_page)
172 self.title_loaded.emit(title)
173
174 def run(self):
175 logging.info("MakeMKV thread started")
176
177 if not os.path.isfile(MAKEMKVCON_PATH):
178 try:
179 self.install()
180 except Exception, e:
181 self.fatal_error.emit(format_exception("Failed to install MakeMKV", e))
182 raise
183
184 try:
185 disc_number = self.find_disc()
186 except Exception, e:
187 self.fatal_error.emit(format_exception("Error searching for disc", e))
188 raise
189
190 if not disc_number:
191 self.fatal_error.emit("No disc found, please insert a disc and try again.")
192 return
193
194 try:
195 self.run_stream(disc_number)
196 except Exception, e:
197 self.fatal_error.emit(format_exception("Failed to start MakeMKV", e))
198 raise
199
200 logging.info("MakeMKV thread finished")
201
202
203 class PlayerWindow(QWidget):
204 def __init__(self):
205 QWidget.__init__(self)
206 self.title_map = {}
207
208 self.list_widget = QListWidget(self)
209 self.list_widget.itemActivated.connect(self.handle_activated)
210 self.list_widget.setFocus()
211
212 self.log = QTextEdit(self)
213 self.log.setReadOnly(True)
214
215 self.splitter = QSplitter(Qt.Vertical, self)
216 self.splitter.addWidget(self.list_widget)
217 self.splitter.addWidget(self.log)
218 self.splitter.setSizes([900, 200])
219
220 self.layout = QVBoxLayout(self)
221 self.layout.addWidget(self.splitter)
222 self.setWindowTitle("BluPlayer")
223
224 def add_title(self, title):
225 name = "Title %s (%s)" % (title.id, title.duration)
226 self.list_widget.addItem(name)
227 if not self.title_map:
228 # select the first item
229 self.list_widget.setCurrentItem(self.list_widget.item(0))
230 self.title_map[name] = title
231
232 def add_log_entry(self, text):
233 self.log.append(text)
234
235 def popup_fatal_error(self, text):
236 QMessageBox.critical(None, "Fatal error", text)
237 qApp.quit()
238
239 def handle_activated(self, item):
240 name = str(item.text())
241 title = self.title_map[name]
242 try:
243 title.play()
244 except Exception, e:
245 popup_error_exit("MPlayer failed to play the video", e)
246
247 def keyPressEvent(self, e):
248 if e.key() in (Qt.Key_Escape, Qt.Key_Backspace):
249 self.close()
250 return
251 QWidget.keyPressEvent(self, e)
252
253
254 def killall_makemkvcon():
255 logging.info("killing makemkvcon")
256 subprocess.Popen(["killall", "makemkvcon"]).wait()
257
258 def main():
259 logging.basicConfig(format="%(levelname)s: %(message)s")
260 logging.getLogger().setLevel(logging.DEBUG)
261
262 app = QApplication(sys.argv)
263 app.setQuitOnLastWindowClosed(True)
264
265 default_font = app.font()
266 default_font.setPointSize(16)
267 app.setFont(default_font)
268
269 player_window = PlayerWindow()
270 player_window.resize(1200, 700)
271 player_window.show()
272
273 makemkv = MakeMkv()
274 worker_thread = QThread()
275 makemkv.moveToThread(worker_thread)
276
277 worker_thread.started.connect(makemkv.run)
278 makemkv.title_loaded.connect(player_window.add_title)
279 makemkv.status.connect(player_window.add_log_entry)
280 makemkv.fatal_error.connect(player_window.popup_fatal_error)
281
282 logging.info("Starting worker thread")
283 worker_thread.start()
284 result = app.exec_()
285
286 logging.info("Shutting down")
287 worker_thread.quit()
288 killall_makemkvcon()
289 logging.info("Waiting for worker thread")
290 worker_thread.wait(2000)
291 logging.info("Exiting...")
292 sys.exit(result)
293
294 if __name__ == "__main__":
295 main()
296