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