]>
code.delx.au - bluplayer/blob - bluplayer.py
d119a3aca21f34f9fbf12ee35d17b62db66919c8
11 from lxml
import etree
12 from PyQt4
.QtCore
import *
13 from PyQt4
.QtGui
import *
16 XHTML
= "http://www.w3.org/1999/xhtml"
20 DEFAULT_PATH
= "/usr/local/bin:/usr/bin:/bin"
23 def set_env_config(name
, value
):
24 value
= os
.environ
.get(name
, value
)
25 globals()[name
] = value
27 def find_path(binary
):
29 paths
= os
.environ
.get("PATH", DEFAULT_PATH
).split(":")
31 path
= os
.path
.join(path
, binary
)
32 if os
.path
.isfile(path
):
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)
43 f
= urllib
.urlopen(url
)
50 tds
= doc
.xpath("//xhtml:td", namespaces
=NS
)
56 if v
.tag
== "{%s}a" % XHTML
:
67 logging
.info("Determing number of CPUs")
69 return subprocess
.check_output("nproc").strip()
71 logging
.warn("Unable to run nproc: %s", fmt_exc(e
))
80 return "%s: %s" % (e
.__class
__.__name
__, str(e
))
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"]
90 return "Title%s: %s %s" % (self
.id, self
.duration
, self
.video_url
)
93 class MakeMkvCon(object):
94 def __init__(self
, params
):
95 cmd
= [MAKEMKVCON_PATH
, "--robot"]
97 logging
.info("Running makemkvcon: %s", params
)
98 self
.p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
99 self
.fd
= self
.p
.stdout
.fileno()
101 self
.finished
= False
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)
116 raise StopIteration()
118 pos
= self
.buf
.find("\n")
120 result
= self
.buf
[:pos
]
121 self
.buf
= self
.buf
[pos
+1:]
122 return self
.decode_line(result
)
125 data
= os
.read(self
.p
.stdout
.fileno(), 4096)
127 if e
.errno
== errno
.EINTR
:
130 self
.status
= self
.p
.wait()
131 logging
.info("makemkvcon exited with status %s", self
.status
)
133 raise StopIteration()
138 class MPlayer(QObject
):
139 play_finished
= pyqtSignal()
140 fatal_error
= pyqtSignal(str, str)
143 logging
.info("mplayer thread started")
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."
155 self
.opts
= MPLAYER_OPTS
.split()
159 "-lavdopts", "threads=%s" % get_num_cpus(),
163 def play(self
, video_url
):
164 video_url
= str(video_url
)
165 logging
.info("Running mplayer: %s", video_url
)
167 subprocess
.check_call([MPLAYER_PATH
] + self
.opts
+ [video_url
])
169 self
.fatal_error
.emit("MPlayer failed to play the video.", fmt_exc(e
))
171 self
.play_finished
.emit()
174 class MakeMkv(QObject
):
175 title_loaded
= pyqtSignal(Title
)
176 title_load_complete
= pyqtSignal()
177 status
= pyqtSignal(str)
178 fatal_error
= pyqtSignal(str, str)
181 self
.url
= "http://192.168.1.114:51001/"
182 makemkvcon
= MakeMkvCon(["info", "disc:9999"])
184 for key
, line
in makemkvcon
:
185 if key
== "MSG" and line
[0] != "5010":
186 self
.status
.emit(line
[3])
188 if disc_number
is None and key
== "DRV" and line
[5]:
189 disc_number
= line
[0]
191 self
.status
.emit("Found disc %s" % disc_name
)
193 if makemkvcon
.status
== 0:
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
)
205 self
.status
.emit(line
[3])
207 if makemkvcon
.status
!= 0:
208 self
.fatal_error
.emit(
209 "MakeMKV quit unexpectedly.",
210 "makemkvcon exited with code %s" % makemkvcon
.status
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()
224 logging
.info("makemkv thread started")
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."
236 disc_number
= self
.find_disc()
238 self
.fatal_error
.emit("Error searching for disc.", fmt_exc(e
))
242 self
.fatal_error
.emit(
244 "Please insert a BluRay disc and try again."
249 self
.run_stream(disc_number
)
251 self
.fatal_error
.emit("Failed to start MakeMKV.", fmt_exc(e
))
254 logging
.info("makemkv thread finished")
257 class PlayerWindow(QWidget
):
258 fatal_error
= pyqtSignal(str, str)
259 video_selected
= pyqtSignal(str)
262 QWidget
.__init
__(self
)
265 self
.is_playing
= False
267 self
.list_widget
= QListWidget(self
)
268 self
.list_widget
.itemActivated
.connect(self
.handle_activated
)
269 self
.list_widget
.setEnabled(False)
271 self
.log
= QTextEdit(self
)
272 self
.log
.setReadOnly(True)
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])
279 self
.layout
= QVBoxLayout(self
)
280 self
.layout
.addWidget(self
.splitter
)
281 self
.setWindowTitle("BluPlayer")
282 self
.resize(1200, 700)
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
289 def select_longest_title(self
):
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
299 self
.list_widget
.setCurrentItem(longest_item
)
300 self
.list_widget
.setEnabled(True)
301 self
.list_widget
.setFocus()
303 def add_log_entry(self
, text
):
304 self
.log
.append(text
)
306 def handle_activated(self
, item
):
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
)
315 def set_play_finished(self
):
316 self
.is_playing
= False
317 self
.list_widget
.setEnabled(True)
318 self
.list_widget
.setFocus()
320 def keyPressEvent(self
, e
):
321 if e
.key() in (Qt
.Key_Escape
, Qt
.Key_Backspace
):
324 QWidget
.keyPressEvent(self
, e
)
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")
336 class ErrorDialog(QMessageBox
):
337 fatal_error
= pyqtSignal(str, str)
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")
347 def configure_popup(self
, text
, detail
):
352 self
.setInformativeText(detail
)
353 QTimer
.singleShot(0, self
.show_and_exit
)
355 def show_and_exit(self
):
356 logging
.info("showing and exiting")
360 def killall_makemkvcon():
361 logging
.info("Stopping any makemkvcon processes")
362 subprocess
.Popen(["killall", "--quiet", "makemkvcon"]).wait()
365 logging
.basicConfig(format
="%(levelname)s: %(message)s")
366 logging
.getLogger().setLevel(logging
.DEBUG
)
367 logging
.info("Configuring application")
369 app
= QApplication(sys
.argv
)
370 app
.setQuitOnLastWindowClosed(True)
372 default_font
= app
.font()
373 default_font
.setPointSize(16)
374 app
.setFont(default_font
)
376 player_window
= PlayerWindow()
379 loading_dialog
= LoadingDialog(player_window
)
380 loading_dialog
.show()
382 error_dialog
= ErrorDialog(player_window
)
385 makemkv_thread
= QThread()
386 makemkv
.moveToThread(makemkv_thread
)
387 makemkv_thread
.started
.connect(makemkv
.run
)
390 mplayer_thread
= QThread()
391 mplayer
.moveToThread(mplayer_thread
)
392 mplayer_thread
.started
.connect(mplayer
.run
)
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
)
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
)
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
)
408 logging
.info("Starting application")
409 makemkv_thread
.start()
410 mplayer_thread
.start()
413 logging
.info("Shutting down")
414 makemkv_thread
.quit()
415 mplayer_thread
.quit()
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...")
424 if __name__
== "__main__":