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
12 import cPickle
as pickle
24 # on the tablet, it's probably in "gnome"
25 from gnome
import gconf
32 # At the moment, we don't have gettext support, so
33 # make a dummy "_" function to passthrough the string
36 running_on_tablet
= os
.path
.exists('/etc/osso_software_version')
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'
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
61 """ A very simple log function (no log output is produced when
62 using the python optimization (-O, -OO) options) """
65 if __debug__
or debug_override
:
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
83 gtk
.icon_size_register('panucci-button', 32, 32)
84 def image(widget
, filename
, is_stock
=False):
85 widget
.remove(widget
.get_child())
88 image
= gtk
.image_new_from_stock(filename
, gtk
.icon_size_from_name('panucci-button'))
90 filename
= find_image(filename
)
91 if filename
is not None:
92 image
= gtk
.image_new_from_file(filename
)
96 image
.set_padding(20, 20)
98 image
.set_padding(5, 5)
102 class PositionManager(object):
103 def __init__(self
, filename
=None):
105 filename
= os
.path
.expanduser('~/.rmp-bookmarks')
106 self
.filename
= filename
109 # load the playback positions
110 f
= open(self
.filename
, 'rb')
111 self
.positions
= pickle
.load(f
)
114 # let's start out with a new dict
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']
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']
142 # save the playback position dict
143 f
= open(self
.filename
, 'wb')
144 pickle
.dump(self
.positions
, f
)
147 pm
= PositionManager()
149 class BookmarksWindow(gtk
.Window
):
150 def __init__(self
, main
):
152 gtk
.Window
.__init
__(self
, gtk
.WINDOW_TOPLEVEL
)
153 self
.set_title('Bookmarks')
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
)
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)
204 for label
, pos
in pm
.get_bookmarks(self
.main
.filename
):
205 self
.add_bookmark(label
=label
, pos
=pos
)
210 for row
in self
.model
:
211 bookmarks
.append((row
[0], row
[2]))
212 pm
.set_bookmarks(self
.main
.filename
, bookmarks
)
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
)
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()
230 def jump_bookmark(self
, w
):
231 selection
= self
.treeview
.get_selection()
232 (model
, iter) = selection
.get_selected()
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
)
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:
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",
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
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
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
):
324 if running_on_tablet
:
325 self
.app
= hildon
.Program()
326 window
= hildon
.Window()
327 self
.app
.add_window(window
)
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())
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)
353 main_hbox
= gtk
.HBox()
354 main_hbox
.set_spacing(6)
355 if running_on_tablet
:
356 window
.add(main_hbox
)
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)
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
)
456 self
.volume
= gtk
.VolumeButton()
457 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
458 buttonbox
.add(self
.volume
)
461 def create_menu(self
):
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
)
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
)
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):
539 (pos
, dur
) = self
.player_get_position()
541 pos
= pm
.get_position(self
.filename
)
542 text
= self
.convert_ns(pos
)
545 def destroy(self
, widget
):
547 self
.gconf
.sset( 'volume', self
.get_volume() )
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
)
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())
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
603 return self
.volume
.get_value()
605 def set_volume(self
, vol
):
606 """ vol is a float from 0 to 1 """
608 if running_on_tablet
:
609 self
.volume
.set_level(vol
*100.0)
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():
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
625 def toggle_volumebar(self
, widget
=None):
626 if self
.volume_timer_id
is None:
627 self
.__set
_volume
_hide
_timer
(5)
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 )
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
):
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
):
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)
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
]:
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
)
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
)
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()
762 self
.playing
= not self
.playing
765 self
.start_progress_timer()
766 self
.player
.set_state(gst
.STATE_PLAYING
)
767 image(self
.button
, 'media-playback-pause.png')
769 self
.stop_progress_timer() # This should save some power
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:
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
)
797 log('No seek parameters specified.')
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
807 def player_get_position(self
):
808 """ returns [ current position, total duration ] """
810 pos_int
= self
.player
.query_position(self
.time_format
, None)[0]
811 dur_int
= self
.player
.query_duration(self
.time_format
, None)[0]
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
)
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
):
838 if t
== gst
.MESSAGE_EOS
:
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
) )
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
))
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
):
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()
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 ):
889 pbl
= gtk
.gdk
.PixbufLoader()
891 pbl
.write(value
.data
)
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
899 traceback
.print_exc(file=sys
.stdout
)
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)
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] + '...'
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")
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
938 _hours
= time_int
/ 3600
939 time_int
= time_int
- (_hours
* 3600)
940 time_str
= str(_hours
) + ":"
942 _mins
= time_int
/ 60
943 time_int
= time_int
- (_mins
* 60)
944 time_str
= time_str
+ str(_mins
) + ":"
946 _mins
= time_int
/ 60
947 time_int
= time_int
- (_mins
* 60)
948 time_str
= time_str
+ "0" + str(_mins
) + ":"
950 time_str
= time_str
+ "00:"
952 time_str
= time_str
+ str(time_int
)
954 time_str
= time_str
+ "0" + str(time_int
)
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
)
967 # save position manager data
970 if __name__
== '__main__':
971 log( 'WARNING: Use the "panucci" executable to run this program.' )