Add a --debug switch on the command line
[panucci.git] / src / panucci / panucci.py
blob2010ecbd0b5fa7da0d45d6ae2b3d770b63cd25f0
1 #!/usr/bin/env python
2 # A resuming media player for Podcasts and Audiobooks
3 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
5 # http://thpinfo.com/2008/panucci/
7 # based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html
9 import sys
10 import os, os.path
11 import time
12 import cPickle as pickle
13 import webbrowser
15 import gtk
16 import gobject
17 import pygst
18 pygst.require('0.10')
19 import gst
21 try:
22 import gconf
23 except:
24 # on the tablet, it's probably in "gnome"
25 from gnome import gconf
27 import dbus
28 import dbus.service
29 import dbus.mainloop
30 import dbus.glib
32 # At the moment, we don't have gettext support, so
33 # make a dummy "_" function to passthrough the string
34 _ = lambda s: s
36 running_on_tablet = os.path.exists('/etc/osso_software_version')
38 try:
39 import hildon
40 except:
41 if running_on_tablet:
42 log('Using GTK widgets, install "python2.5-hildon" for this to work properly.')
44 about_name = 'Panucci'
45 about_text = _('Resuming audiobook and podcast player')
46 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
47 about_website = 'http://thpinfo.com/2008/panucci/'
48 donate_wishlist_url = 'http://www.amazon.de/gp/registry/2PD2MYGHE6857'
49 donate_device_url = 'http://maemo.gpodder.org/donate.html'
51 short_seek = 10
52 long_seek = 60
54 gconf_dir = '/apps/panucci'
56 coverart_names = [ 'cover', 'cover.jpg', 'cover.png' ]
57 coverart_size = [240, 240] if running_on_tablet else [130, 130]
59 debug_override = False
60 def log( msg ):
61 """ A very simple log function (no log output is produced when
62 using the python optimization (-O, -OO) options) """
63 global debug_override
65 if __debug__ or debug_override:
66 print msg
68 def open_link(d, url, data):
69 webbrowser.open_new(url)
71 gtk.about_dialog_set_url_hook(open_link, None)
74 def find_image(filename):
75 locations = ['./icons/', '../icons/', '/usr/share/panucci/', os.path.dirname(sys.argv[0])+'/../icons/']
77 for location in locations:
78 if os.path.exists(location+filename):
79 return location+filename
81 return None
83 gtk.icon_size_register('panucci-button', 32, 32)
84 def image(widget, filename, is_stock=False):
85 widget.remove(widget.get_child())
86 image = None
87 if is_stock:
88 image = gtk.image_new_from_stock(filename, gtk.icon_size_from_name('panucci-button'))
89 else:
90 filename = find_image(filename)
91 if filename is not None:
92 image = gtk.image_new_from_file(filename)
94 if image is not None:
95 if running_on_tablet:
96 image.set_padding(20, 20)
97 else:
98 image.set_padding(5, 5)
99 widget.add(image)
100 image.show()
102 class PositionManager(object):
103 def __init__(self, filename=None):
104 if filename is None:
105 filename = os.path.expanduser('~/.rmp-bookmarks')
106 self.filename = filename
108 try:
109 # load the playback positions
110 f = open(self.filename, 'rb')
111 self.positions = pickle.load(f)
112 f.close()
113 except:
114 # let's start out with a new dict
115 self.positions = {}
117 def set_position(self, url, position):
118 if not url in self.positions:
119 self.positions[url] = {}
121 self.positions[url]['position'] = position
123 def get_position(self, url):
124 if url in self.positions and 'position' in self.positions[url]:
125 return self.positions[url]['position']
126 else:
127 return 0
129 def set_bookmarks(self, url, bookmarks):
130 if not url in self.positions:
131 self.positions[url] = {}
133 self.positions[url]['bookmarks'] = bookmarks
135 def get_bookmarks(self, url):
136 if url in self.positions and 'bookmarks' in self.positions[url]:
137 return self.positions[url]['bookmarks']
138 else:
139 return []
141 def save(self):
142 # save the playback position dict
143 f = open(self.filename, 'wb')
144 pickle.dump(self.positions, f)
145 f.close()
147 pm = PositionManager()
149 class BookmarksWindow(gtk.Window):
150 def __init__(self, main):
151 self.main = main
152 gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
153 self.set_title('Bookmarks')
154 self.set_modal(True)
155 self.set_default_size(400, 300)
156 self.set_border_width(10)
157 self.vbox = gtk.VBox()
158 self.vbox.set_spacing(5)
159 self.treeview = gtk.TreeView()
160 self.treeview.set_headers_visible(True)
161 self.model = gtk.ListStore(gobject.TYPE_STRING,
162 gobject.TYPE_STRING, gobject.TYPE_UINT64)
163 self.treeview.set_model(self.model)
165 ncol = gtk.TreeViewColumn('Name')
166 ncell = gtk.CellRendererText()
167 ncell.set_property('editable', True)
168 ncell.connect('edited', self.label_edited)
169 ncol.pack_start(ncell)
170 ncol.add_attribute(ncell, 'text', 0)
172 tcol = gtk.TreeViewColumn('Time')
173 tcell = gtk.CellRendererText()
174 tcol.pack_start(tcell)
175 tcol.add_attribute(tcell, 'text', 1)
177 self.treeview.append_column(ncol)
178 self.treeview.append_column(tcol)
180 sw = gtk.ScrolledWindow()
181 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
182 sw.set_shadow_type(gtk.SHADOW_IN)
183 sw.add(self.treeview)
184 self.vbox.add(sw)
185 self.hbox = gtk.HButtonBox()
186 self.add_button = gtk.Button(gtk.STOCK_ADD)
187 self.add_button.set_use_stock(True)
188 self.add_button.connect('clicked', self.add_bookmark)
189 self.hbox.pack_start(self.add_button)
190 self.remove_button = gtk.Button(gtk.STOCK_REMOVE)
191 self.remove_button.set_use_stock(True)
192 self.remove_button.connect('clicked', self.remove_bookmark)
193 self.hbox.pack_start(self.remove_button)
194 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
195 self.jump_button.set_use_stock(True)
196 self.jump_button.connect('clicked', self.jump_bookmark)
197 self.hbox.pack_start(self.jump_button)
198 self.close_button = gtk.Button(gtk.STOCK_CLOSE)
199 self.close_button.set_use_stock(True)
200 self.close_button.connect('clicked', self.close)
201 self.hbox.pack_start(self.close_button)
202 self.vbox.pack_start(self.hbox, False, True)
203 self.add(self.vbox)
204 for label, pos in pm.get_bookmarks(self.main.filename):
205 self.add_bookmark(label=label, pos=pos)
206 self.show_all()
208 def close(self, w):
209 bookmarks = []
210 for row in self.model:
211 bookmarks.append((row[0], row[2]))
212 pm.set_bookmarks(self.main.filename, bookmarks)
213 self.destroy()
215 def label_edited(self, cellrenderer, path, new_text):
216 self.model.set_value(self.model.get_iter(path), 0, new_text)
218 def add_bookmark(self, w=None, label=None, pos=None):
219 (text, position) = self.main.get_position(pos)
220 if label is None:
221 label = text
222 self.model.append([label, text, position])
224 def remove_bookmark(self, w):
225 selection = self.treeview.get_selection()
226 (model, iter) = selection.get_selected()
227 if iter is not None:
228 model.remove(iter)
230 def jump_bookmark(self, w):
231 selection = self.treeview.get_selection()
232 (model, iter) = selection.get_selected()
233 if iter is not None:
234 pos = model.get_value(iter, 2)
235 self.main.do_seek(from_beginning=pos)
237 class SimpleGConfClient(gconf.Client):
238 """ A simplified wrapper around gconf.Client
239 GConf docs: http://library.gnome.org/devel/gconf/stable/
242 __type_mapping = { int: 'int', long: 'float', float: 'float',
243 str: 'string', bool: 'bool', list: 'list', }
245 def __init__(self, directory):
246 """ directory is the base directory that we're working in """
247 self.__directory = directory
248 gconf.Client.__init__(self)
250 self.add_dir( self.__directory, gconf.CLIENT_PRELOAD_NONE )
252 def __get_manipulator_method( self, data_type, operation ):
253 """ data_type must be a vaild "type"
254 operation is either 'set' or 'get' """
256 if self.__type_mapping.has_key( data_type ):
257 method = operation + '_' + self.__type_mapping[data_type]
258 return getattr( self, method )
259 else:
260 log('Data type "%s" is not supported.' % data_type)
261 return lambda x,y=None: None
263 def sset( self, key, value ):
264 """ A simple set function, no type is required, it is determined
265 automatically. 'key' is relative to self.__directory """
267 return self.__get_manipulator_method(type(value), 'set')(
268 os.path.join(self.__directory, key), value )
270 def sget( self, key, data_type, default=None ):
271 """ A simple get function, type is required, default value is
272 optional, 'key' is relative to self.__directory """
274 if self.get( os.path.join(self.__directory, key) ) is None:
275 return default
276 else:
277 return self.__get_manipulator_method(data_type, 'get')(
278 os.path.join(self.__directory, key) )
280 def snotify( self, callback ):
281 """ Set a callback to watch self.__directory """
282 return self.notify_add( self.__directory, callback )
284 class GTK_Main(dbus.service.Object):
286 def __init__(self, bus_name, filename=None):
287 dbus.service.Object.__init__(self, object_path="/player",
288 bus_name=bus_name)
290 self.gconf = SimpleGConfClient( gconf_dir )
291 self.gconf.snotify(self.gconf_key_changed)
293 self.filename = filename
294 self.progress_timer_id = None
295 self.volume_timer_id = None
296 self.make_main_window()
297 self.has_coverart = False
298 self.has_id3_coverart = False
299 self.playing = False
301 if running_on_tablet:
302 # Enable play/pause with headset button
303 system_bus = dbus.SystemBus()
304 headset_button = system_bus.get_object('org.freedesktop.Hal',
305 '/org/freedesktop/Hal/devices/platform_retu_headset_logicaldev_input')
306 headset_device = dbus.Interface(headset_button, 'org.freedesktop.Hal.Device')
307 headset_device.connect_to_signal('Condition', self.handle_headset_button)
309 self.want_to_seek = False
310 self.player = None
312 # Placeholder functions, these are generated dynamically
313 self.get_volume_level = lambda: 0
314 self.set_volume_level = lambda x: 0
316 self.set_volume(self.gconf.sget('volume', float, 0.3))
317 self.time_format = gst.Format(gst.FORMAT_TIME)
318 if self.filename is not None:
319 self.play_file(self.filename)
321 def make_main_window(self):
322 import pango
324 if running_on_tablet:
325 self.app = hildon.Program()
326 window = hildon.Window()
327 self.app.add_window(window)
328 else:
329 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
331 window.set_title('Panucci')
332 self.window_icon = find_image('panucci.png')
333 if self.window_icon is not None:
334 window.set_icon_from_file( self.window_icon )
335 window.set_default_size(400, -1)
336 window.set_border_width(0)
337 window.connect("destroy", self.destroy)
338 self.main_window = window
340 if running_on_tablet:
341 window.set_menu(self.create_menu())
342 else:
343 menu_vbox = gtk.VBox()
344 menu_vbox.set_spacing(0)
345 window.add(menu_vbox)
346 menu_bar = gtk.MenuBar()
347 root_menu = gtk.MenuItem('Panucci')
348 root_menu.set_submenu(self.create_menu())
349 menu_bar.append(root_menu)
350 menu_vbox.pack_start(menu_bar, False, False, 0)
351 menu_bar.show()
353 main_hbox = gtk.HBox()
354 main_hbox.set_spacing(6)
355 if running_on_tablet:
356 window.add(main_hbox)
357 else:
358 menu_vbox.pack_end(main_hbox, True, True, 6)
360 main_vbox = gtk.VBox()
361 main_vbox.set_spacing(6)
362 # add a vbox to the main_hbox
363 main_hbox.pack_start(main_vbox, True, True)
365 # a hbox to hold the cover art and metadata vbox
366 metadata_hbox = gtk.HBox()
367 metadata_hbox.set_spacing(6)
368 main_vbox.pack_start(metadata_hbox, True, False)
370 self.cover_art = gtk.Image()
371 metadata_hbox.pack_start( self.cover_art, False, False )
373 # vbox to hold metadata
374 metadata_vbox = gtk.VBox()
375 metadata_vbox.set_spacing(8)
376 empty_label = gtk.Label()
377 metadata_vbox.pack_start(empty_label, True, True)
378 self.artist_label = gtk.Label('')
379 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
380 metadata_vbox.pack_start(self.artist_label, False, False)
381 self.album_label = gtk.Label('')
382 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
383 metadata_vbox.pack_start(self.album_label, False, False)
384 self.title_label = gtk.Label('')
385 self.title_label.set_line_wrap(True)
386 metadata_vbox.pack_start(self.title_label, False, False)
387 empty_label = gtk.Label()
388 metadata_vbox.pack_start(empty_label, True, True)
389 metadata_hbox.pack_start( metadata_vbox, True, True )
391 progress_eventbox = gtk.EventBox()
392 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
393 progress_eventbox.connect('button-press-event', self.on_progressbar_changed)
394 self.progress = gtk.ProgressBar()
395 if running_on_tablet: # make the progress bar more "finger-friendly"
396 self.progress.set_size_request( -1, 50 )
397 progress_eventbox.add(self.progress)
398 main_vbox.pack_start( progress_eventbox, False, False )
400 # make the button box
401 buttonbox = gtk.HBox()
402 self.rrewind_button = gtk.Button('')
403 image(self.rrewind_button, 'media-skip-backward.png')
404 self.rrewind_button.connect('clicked', self.seekbutton_callback, -1*long_seek)
405 buttonbox.add(self.rrewind_button)
406 self.rewind_button = gtk.Button('')
407 image(self.rewind_button, 'media-seek-backward.png')
408 self.rewind_button.connect('clicked', self.seekbutton_callback, -1*short_seek)
409 buttonbox.add(self.rewind_button)
410 self.button = gtk.Button('')
411 image(self.button, gtk.STOCK_OPEN, True)
412 self.button.connect('clicked', self.start_stop)
413 buttonbox.add(self.button)
414 self.forward_button = gtk.Button('')
415 image(self.forward_button, 'media-seek-forward.png')
416 self.forward_button.connect('clicked', self.seekbutton_callback, short_seek)
417 buttonbox.add(self.forward_button)
418 self.fforward_button = gtk.Button('')
419 image(self.fforward_button, 'media-skip-forward.png')
420 self.fforward_button.connect('clicked', self.seekbutton_callback, long_seek)
421 buttonbox.add(self.fforward_button)
422 self.bookmarks_button = gtk.Button('')
423 image(self.bookmarks_button, 'bookmark-new.png')
424 self.bookmarks_button.connect('clicked', self.bookmarks_callback)
425 buttonbox.add(self.bookmarks_button)
426 self.set_controls_sensitivity(False)
427 main_vbox.pack_start(buttonbox, False, False)
429 window.show_all()
431 if running_on_tablet:
432 self.volume = hildon.VVolumebar()
433 self.volume.set_property('can-focus', False)
434 self.volume.connect('level_changed', self.volume_changed_hildon)
435 self.volume.connect('mute_toggled', self.mute_toggled)
436 window.connect('key-press-event', self.on_key_press)
437 main_hbox.pack_start(self.volume, False, True)
439 # Add a button to pop out the volume bar
440 self.volume_button = gtk.ToggleButton('')
441 image(self.volume_button, 'media-speaker.png')
442 self.volume_button.connect('clicked', self.toggle_volumebar)
443 self.volume.connect('show', lambda x: self.volume_button.set_active(True))
444 self.volume.connect('hide', lambda x: self.volume_button.set_active(False))
445 buttonbox.add(self.volume_button)
446 self.volume_button.show()
448 # Disable focus for all widgets, so we can use the cursor
449 # keys + enter to directly control our media player, which
450 # is handled by "key-press-event"
451 for w in (self.rrewind_button, self.rewind_button, self.button,
452 self.forward_button, self.fforward_button, self.bookmarks_button,
453 self.volume_button, self.progress):
454 w.unset_flags(gtk.CAN_FOCUS)
455 else:
456 self.volume = gtk.VolumeButton()
457 self.volume.connect('value-changed', self.volume_changed_gtk)
458 buttonbox.add(self.volume)
459 self.volume.show()
461 def create_menu(self):
462 # the main menu
463 menu = gtk.Menu()
465 menu_open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
466 menu_open.connect("activate", self.open_file_callback)
467 menu.append(menu_open)
469 menu.append(gtk.SeparatorMenuItem())
471 menu_bookmarks = gtk.MenuItem(_('Bookmarks'))
472 menu_bookmarks.connect('activate', self.bookmarks_callback)
473 menu.append(menu_bookmarks)
476 # the settings sub-menu
477 menu_settings = gtk.MenuItem(_('Settings'))
478 menu.append(menu_settings)
480 menu_settings_sub = gtk.Menu()
481 menu_settings.set_submenu(menu_settings_sub)
483 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
484 menu_settings_lock_progress.connect('toggled', lambda w:
485 self.gconf.sset('progress_locked', w.get_active()))
486 menu_settings_lock_progress.set_active(self.lock_progress)
487 menu_settings_sub.append(menu_settings_lock_progress)
489 menu.append(gtk.SeparatorMenuItem())
491 # the donate sub-menu
492 menu_donate = gtk.MenuItem(_('Donate'))
493 menu.append(menu_donate)
495 menu_donate_sub = gtk.Menu()
496 menu_donate.set_submenu(menu_donate_sub)
498 menu_donate_device = gtk.MenuItem(_('Developer device'))
499 menu_donate_device.connect("activate", lambda w: webbrowser.open_new(donate_device_url))
500 menu_donate_sub.append(menu_donate_device)
502 menu_donate_wishlist = gtk.MenuItem(_('Amazon Wishlist'))
503 menu_donate_wishlist.connect("activate", lambda w: webbrowser.open_new(donate_wishlist_url))
504 menu_donate_sub.append(menu_donate_wishlist)
506 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
507 menu_about.connect("activate", self.show_about, self.main_window)
508 menu.append(menu_about)
510 menu.append(gtk.SeparatorMenuItem())
512 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
513 menu_quit.connect("activate", self.destroy)
514 menu.append(menu_quit)
516 return menu
518 @property
519 def lock_progress(self):
520 return self.gconf.sget('progress_locked', bool)
522 def show_about(self, w, win):
523 dialog = gtk.AboutDialog()
524 dialog.set_website(about_website)
525 dialog.set_website_label(about_website)
526 dialog.set_name(about_name)
527 dialog.set_authors(about_authors)
528 dialog.set_comments(about_text)
529 dialog.run()
530 dialog.destroy()
532 def save_position(self):
533 (pos, dur) = self.player_get_position()
534 pm.set_position(self.filename, pos)
536 def get_position(self, pos=None):
537 if pos is None:
538 if self.playing:
539 (pos, dur) = self.player_get_position()
540 else:
541 pos = pm.get_position(self.filename)
542 text = self.convert_ns(pos)
543 return (text, pos)
545 def destroy(self, widget):
546 self.stop_playing()
547 self.gconf.sset( 'volume', self.get_volume() )
548 gtk.main_quit()
550 def gconf_key_changed(self, client, connection_id, entry, args):
551 log( 'gconf key %s changed: %s' % (entry.get_key(), entry.get_value()))
553 def handle_headset_button(self, event, button):
554 if event == 'ButtonPressed' and button == 'phone':
555 self.start_stop(self.button)
557 def get_file_from_filechooser(self):
558 if running_on_tablet:
559 dlg = hildon.FileChooserDialog(self.main_window,
560 gtk.FILE_CHOOSER_ACTION_OPEN)
561 else:
562 dlg = gtk.FileChooserDialog(_('Select podcast or audiobook'),
563 None, gtk.FILE_CHOOSER_ACTION_OPEN, ((gtk.STOCK_CANCEL,
564 gtk.RESPONSE_REJECT, gtk.STOCK_MEDIA_PLAY, gtk.RESPONSE_OK)))
566 current_folder = self.gconf.sget('last_folder',str)
567 if current_folder is not None and os.path.isdir(current_folder):
568 dlg.set_current_folder(current_folder)
570 if dlg.run() == gtk.RESPONSE_OK:
571 filename = dlg.get_filename()
572 self.gconf.sset('last_folder', dlg.get_current_folder())
573 dlg.destroy()
574 else:
575 filename = None
577 dlg.destroy()
578 return filename
580 def set_controls_sensitivity(self, sensitive):
581 self.forward_button.set_sensitive(sensitive)
582 self.rewind_button.set_sensitive(sensitive)
583 self.fforward_button.set_sensitive(sensitive)
584 self.rrewind_button.set_sensitive(sensitive)
586 def on_key_press(self, widget, event):
587 if event.keyval == gtk.keysyms.F7: #plus
588 self.set_volume( min( 1, self.get_volume() + 0.10 ))
589 elif event.keyval == gtk.keysyms.F8: #minus
590 self.set_volume( max( 0, self.get_volume() - 0.10 ))
591 elif event.keyval == gtk.keysyms.Left: # seek back
592 self.rewind_callback(self.rewind_button)
593 elif event.keyval == gtk.keysyms.Right: # seek forward
594 self.forward_callback(self.forward_button)
595 elif event.keyval == gtk.keysyms.Return: # play/pause
596 self.start_stop(self.button)
598 # The following two functions get and set the volume from the volume control widgets
599 def get_volume(self):
600 if running_on_tablet:
601 return self.volume.get_level()/100.0
602 else:
603 return self.volume.get_value()
605 def set_volume(self, vol):
606 """ vol is a float from 0 to 1 """
607 assert 0 <= vol <= 1
608 if running_on_tablet:
609 self.volume.set_level(vol*100.0)
610 else:
611 self.volume.set_value(vol)
613 def __set_volume_hide_timer(self, timeout, force_show=False):
614 if force_show or self.volume_button.get_active():
615 self.volume.show()
616 if self.volume_timer_id is not None:
617 gobject.source_remove(self.volume_timer_id)
618 self.volume_timer_id = gobject.timeout_add(1000*timeout, self.__volume_hide_callback)
620 def __volume_hide_callback(self):
621 self.volume_timer_id = None
622 self.volume.hide()
623 return False
625 def toggle_volumebar(self, widget=None):
626 if self.volume_timer_id is None:
627 self.__set_volume_hide_timer(5)
628 else:
629 self.__volume_hide_callback()
631 def volume_changed_gtk(self, widget, new_value=0.5):
632 self.set_volume_level( new_value )
634 def volume_changed_hildon(self, widget):
635 self.__set_volume_hide_timer( 4, force_show=True )
636 self.set_volume_level( widget.get_level()/100.0 )
638 def mute_toggled(self, widget):
639 if widget.get_mute():
640 self.set_volume_level( 0 )
641 else:
642 self.set_volume_level( widget.get_level()/100.0 )
644 @dbus.service.method('org.panucci.interface')
645 def show_main_window(self):
646 self.main_window.present()
648 @dbus.service.method('org.panucci.interface', in_signature='s')
649 def play_file(self, filename):
650 self.stop_playing()
652 self.filename = os.path.abspath(filename)
653 pretty_filename = os.path.basename(self.filename).rsplit('.',1)[0].replace('_', ' ')
654 self.setup_player(self.filename)
656 self.has_coverart = False
657 self.want_to_seek = True
658 self.start_playback()
659 self.start_progress_timer()
661 # This is just in case the file contains no tags,
662 # at least we can display the filename
663 self.set_metadata({'title': pretty_filename})
665 def open_file_callback(self, widget=None):
666 old_filename = self.filename
667 filename = self.get_file_from_filechooser()
668 if filename is not None:
669 self.play_file(filename)
671 @dbus.service.method('org.panucci.interface')
672 def stop_playing(self):
673 if self.playing:
674 self.start_stop(widget=None)
676 if self.player is not None: self.player.set_state(gst.STATE_NULL)
677 self.stop_progress_timer()
678 self.title_label.set_size_request(-1,-1)
679 self.filename = None
680 self.playing = False
681 self.has_coverart = False
682 self.has_id3_coverart = False
683 self.reset_progress()
684 self.set_controls_sensitivity(False)
685 image(self.button, gtk.STOCK_OPEN, True)
687 def start_playback(self):
688 self.set_controls_sensitivity(True)
689 for widget in [ self.title_label, self.artist_label, self.album_label ]:
690 widget.set_text('')
691 widget.hide()
692 self.cover_art.hide()
693 self.start_stop(widget=None)
695 def setup_player(self, filename):
696 if filename.lower().endswith('.ogg') and running_on_tablet:
697 log( 'Using OGG workaround, I hope this works...' )
699 self.player = gst.Pipeline('player')
700 source = gst.element_factory_make('gnomevfssrc', 'file-source')
701 audio_decoder = gst.element_factory_make('tremor', 'vorbis-decoder')
702 self.__volume_control = gst.element_factory_make('volume', 'volume')
703 audiosink = gst.element_factory_make('dsppcmsink', 'audio-output')
705 self.player.add(source, audio_decoder, self.__volume_control, audiosink)
706 gst.element_link_many(source, audio_decoder, self.__volume_control, audiosink)
708 self.get_volume_level = lambda : self.__get_volume_level(self.__volume_control)
709 self.set_volume_level = lambda x: self.__set_volume_level(x, self.__volume_control)
711 source.set_property( 'location', 'file://' + filename )
712 else:
713 log( 'Using plain-old playbin.' )
715 self.player = gst.element_factory_make('playbin', 'player')
717 # Workaround for volume on maemo, they use a 0 to 10 scale
718 div = int(running_on_tablet)*10 or 1
719 self.get_volume_level = lambda : self.__get_volume_level(self.player, div)
720 self.set_volume_level = lambda x: self.__set_volume_level(x, self.player, div)
722 self.player.set_property( 'uri', 'file://' + self.filename )
724 bus = self.player.get_bus()
725 bus.add_signal_watch()
726 bus.connect("message", self.on_message)
728 self.set_volume_level(self.get_volume())
730 def __get_volume_level(self, volume_control, divisor=1):
731 vol = volume_control.get_property('volume') / float(divisor)
732 assert 0 <= vol <= 1
733 return vol
735 def __set_volume_level(self, value, volume_control, multiplier=1):
736 assert 0 <= value <= 1
737 volume_control.set_property('volume', value * float(multiplier))
739 def reset_progress(self):
740 self.progress.set_fraction(0)
741 self.set_progress_callback(0,0)
743 def set_progress_callback(self, time_elapsed, total_time):
744 """ times must be in nanoseconds """
745 time_string = "%s / %s" % ( self.convert_ns(time_elapsed),
746 self.convert_ns(total_time) )
747 self.progress.set_text( time_string )
748 fraction = float(time_elapsed) / float(total_time) if total_time else 0
749 self.progress.set_fraction( fraction )
751 def on_progressbar_changed(self, widget, event):
752 if ( not self.lock_progress and
753 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
754 new_fraction = event.x/float(widget.get_allocation().width)
755 self.do_seek(percent=new_fraction)
757 def start_stop(self, widget=None):
758 if self.filename is None or not os.path.exists(self.filename):
759 self.open_file_callback()
760 return
762 self.playing = not self.playing
764 if self.playing:
765 self.start_progress_timer()
766 self.player.set_state(gst.STATE_PLAYING)
767 image(self.button, 'media-playback-pause.png')
768 else:
769 self.stop_progress_timer() # This should save some power
770 self.save_position()
771 self.player.set_state(gst.STATE_PAUSED)
772 image(self.button, 'media-playback-start.png')
774 def do_seek(self, from_beginning=None, from_current=None, percent=None ):
775 """ Takes one of the following keyword arguments:
776 from_beginning=n: seek n nanoseconds from the beinging of the file
777 from_current=n: seek n nanoseconds from the current position
778 percent=n: seek n percent from the beginning of the file
780 self.want_to_seek = True
782 position, duration = self.player_get_position()
783 # if position and duration are 0 then player_get_position caught an
784 # exception. Therefore self.player isn't ready to be seeing.
785 if not ( position or duration ) or self.player is None:
786 return False
788 if from_beginning is not None:
789 assert from_beginning >= 0
790 position = min( from_beginning, duration )
791 elif from_current is not None:
792 position = max( 0, min( position+from_current, duration ))
793 elif percent is not None:
794 assert 0 <= percent <= 1
795 position = int(duration*percent)
796 else:
797 log('No seek parameters specified.')
798 return False
800 # Preemptively update the progressbar to make seeking nice and smooth
801 self.set_progress_callback( position, duration )
802 self.player.seek_simple(self.time_format, gst.SEEK_FLAG_FLUSH, position)
803 self.want_to_seek = False
805 return True
807 def player_get_position(self):
808 """ returns [ current position, total duration ] """
809 try:
810 pos_int = self.player.query_position(self.time_format, None)[0]
811 dur_int = self.player.query_duration(self.time_format, None)[0]
812 except:
813 pos_int = dur_int = 0
814 return pos_int, dur_int
816 def progress_timer_callback( self ):
817 if self.playing and not self.want_to_seek:
818 pos_int, dur_int = self.player_get_position()
819 # This prevents bogus values from being set while seeking
820 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
821 self.set_progress_callback( pos_int, dur_int )
822 return True
824 def start_progress_timer( self ):
825 if self.progress_timer_id is not None:
826 self.stop_progress_timer()
828 self.progress_timer_id = gobject.timeout_add( 1000, self.progress_timer_callback )
830 def stop_progress_timer( self ):
831 if self.progress_timer_id is not None:
832 gobject.source_remove( self.progress_timer_id )
833 self.progress_timer_id = None
835 def on_message(self, bus, message):
836 t = message.type
838 if t == gst.MESSAGE_EOS:
839 self.stop_playing()
840 pm.set_position(self.filename, 0)
842 elif t == gst.MESSAGE_ERROR:
843 err, debug = message.parse_error()
844 log( "Error: %s %s" % (err, debug) )
845 self.stop_playing()
847 elif t == gst.MESSAGE_STATE_CHANGED:
848 if ( message.src == self.player and
849 message.structure['new-state'] == gst.STATE_PLAYING ):
851 if self.want_to_seek:
852 # This only gets called when the file is first loaded
853 self.do_seek(from_beginning=pm.get_position(self.filename))
854 else:
855 self.set_controls_sensitivity(True)
857 elif t == gst.MESSAGE_TAG:
858 keys = message.parse_tag().keys()
859 tags = dict([ (key, message.structure[key]) for key in keys ])
860 self.set_metadata( tags )
862 def set_coverart( self, pixbuf ):
863 self.cover_art.set_from_pixbuf(pixbuf)
864 self.cover_art.show()
865 self.has_coverart = True
867 def set_coverart_from_dir( self, directory ):
868 for cover in coverart_names:
869 c = os.path.join( directory, cover )
870 if os.path.isfile(c):
871 try:
872 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(c, *coverart_size)
873 self.cover_art.set_from_pixbuf(pixbuf)
874 self.cover_art.show()
875 return True
876 except:
877 pass
878 return False
880 def set_metadata( self, tag_message ):
881 tags = { 'title': self.title_label, 'artist': self.artist_label,
882 'album': self.album_label }
884 if tag_message.has_key('image') and not self.has_id3_coverart:
885 value = tag_message['image']
886 if isinstance( value, list ):
887 value = value[0]
889 pbl = gtk.gdk.PixbufLoader()
890 try:
891 pbl.write(value.data)
892 pbl.close()
893 pixbuf = pbl.get_pixbuf().scale_simple(
894 coverart_size[0], coverart_size[1], gtk.gdk.INTERP_BILINEAR )
895 self.set_coverart(pixbuf)
896 self.has_id3_coverart = True
897 except:
898 import traceback
899 traceback.print_exc(file=sys.stdout)
900 pbl.close()
902 if not self.has_coverart and self.filename is not None:
903 self.has_coverart = self.set_coverart_from_dir(os.path.dirname(self.filename))
905 tag_vals = dict([ (i,'') for i in tags.keys()])
906 for tag,value in tag_message.iteritems():
907 if tags.has_key(tag) and value.strip():
908 tags[tag].set_markup('<big>'+value+'</big>')
909 tag_vals[tag] = value
910 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
911 tags[tag].show()
912 if tag == 'title':
913 if running_on_tablet:
914 self.main_window.set_title(value)
915 # oh man this is hacky :(
916 if self.has_coverart:
917 tags[tag].set_size_request(420,-1)
918 if len(value) >= 80: value = value[:80] + '...'
919 else:
920 self.main_window.set_title('Panucci - ' + value)
922 tags[tag].set_markup('<b><big>'+value+'</big></b>')
924 def demuxer_callback(self, demuxer, pad):
925 adec_pad = self.audio_decoder.get_pad("sink")
926 pad.link(adec_pad)
928 def seekbutton_callback( self, widget, seek_amount ):
929 self.do_seek(from_current=seek_amount*10**9)
931 def bookmarks_callback(self, w):
932 BookmarksWindow(self)
934 def convert_ns(self, time_int):
935 time_int = time_int / 1000000000
936 time_str = ""
937 if time_int >= 3600:
938 _hours = time_int / 3600
939 time_int = time_int - (_hours * 3600)
940 time_str = str(_hours) + ":"
941 if time_int >= 600:
942 _mins = time_int / 60
943 time_int = time_int - (_mins * 60)
944 time_str = time_str + str(_mins) + ":"
945 elif time_int >= 60:
946 _mins = time_int / 60
947 time_int = time_int - (_mins * 60)
948 time_str = time_str + "0" + str(_mins) + ":"
949 else:
950 time_str = time_str + "00:"
951 if time_int > 9:
952 time_str = time_str + str(time_int)
953 else:
954 time_str = time_str + "0" + str(time_int)
956 return time_str
959 def run(filename=None, debug=False):
960 global debug_override
961 debug_override = debug
963 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
964 bus_name = dbus.service.BusName('org.panucci', bus=session_bus)
965 GTK_Main(bus_name, filename)
966 gtk.main()
967 # save position manager data
968 pm.save()
970 if __name__ == '__main__':
971 log( 'WARNING: Use the "panucci" executable to run this program.' )
972 log( 'Exiting...' )