Preemptively update the progressbar to make seeking smoother
[panucci.git] / src / panucci / panucci.py
blobc8ae8b8d7c6e677a6f55f3bc7513ecf7d2104dcc
1 #!/usr/bin/env python
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
19 # Based on http://thpinfo.com/2008/panucci/:
20 # A resuming media player for Podcasts and Audiobooks
21 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
26 import logging
27 import sys
28 import os, os.path
29 import time
31 import gtk
32 import gobject
34 # At the moment, we don't have gettext support, so
35 # make a dummy "_" function to passthrough the string
36 _ = lambda s: s
38 log = logging.getLogger('panucci.panucci')
40 import util
42 try:
43 import hildon
44 except:
45 if util.platform == util.MAEMO:
46 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
47 'for this to work properly.' )
49 from simplegconf import gconf
50 from settings import settings
51 from player import player
52 from dbusinterface import interface
54 about_name = 'Panucci'
55 about_text = _('Resuming audiobook and podcast player')
56 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
57 about_website = 'http://panucci.garage.maemo.org/'
58 app_version = ''
59 donate_wishlist_url = 'http://www.amazon.de/gp/registry/2PD2MYGHE6857'
60 donate_device_url = 'http://maemo.gpodder.org/donate.html'
62 short_seek = 10
63 long_seek = 60
65 coverart_names = [ 'cover', 'cover.jpg', 'cover.png' ]
66 coverart_size = [240, 240] if util.platform == util.MAEMO else [130, 130]
68 gtk.about_dialog_set_url_hook(util.open_link, None)
69 gtk.icon_size_register('panucci-button', 32, 32)
71 def image(widget, filename, is_stock=False):
72 widget.remove(widget.get_child())
73 image = None
74 if is_stock:
75 image = gtk.image_new_from_stock(
76 filename, gtk.icon_size_from_name('panucci-button') )
77 else:
78 filename = util.find_image(filename)
79 if filename is not None:
80 image = gtk.image_new_from_file(filename)
82 if image is not None:
83 if util.platform == util.MAEMO:
84 image.set_padding(20, 20)
85 else:
86 image.set_padding(5, 5)
87 widget.add(image)
88 image.show()
90 def dialog( toplevel_window, title, question, description ):
91 """ Present the user with a yes/no/cancel dialog
92 Reponse: Yes = True, No = False, Cancel = None """
94 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
95 gtk.MESSAGE_QUESTION )
96 dlg.set_title(title)
97 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL )
98 dlg.add_button( gtk.STOCK_NO, gtk.RESPONSE_NO )
99 dlg.add_button( gtk.STOCK_YES, gtk.RESPONSE_YES )
100 dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % (
101 question, description ))
103 response = dlg.run()
104 dlg.destroy()
106 if response == gtk.RESPONSE_YES:
107 return True
108 elif response == gtk.RESPONSE_NO:
109 return False
110 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
111 return None
113 def get_file_from_filechooser( toplevel_window, save_file=False, save_to=None):
114 if util.platform == util.MAEMO:
115 if save_file:
116 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
117 else:
118 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_OPEN )
120 dlg = hildon.FileChooserDialog( *args )
121 else:
122 if save_file:
123 args = ( _('Select file to save playlist to'), None,
124 gtk.FILE_CHOOSER_ACTION_SAVE,
125 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
126 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
127 else:
128 args = ( _('Select podcast or audiobook'), None,
129 gtk.FILE_CHOOSER_ACTION_OPEN,
130 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
131 gtk.STOCK_MEDIA_PLAY, gtk.RESPONSE_OK )) )
133 dlg = gtk.FileChooserDialog(*args)
135 current_folder = os.path.expanduser(settings.last_folder)
137 if current_folder is not None and os.path.isdir(current_folder):
138 dlg.set_current_folder(current_folder)
140 if save_file and save_to is not None:
141 dlg.set_current_name(save_to)
143 if dlg.run() == gtk.RESPONSE_OK:
144 filename = dlg.get_filename()
145 settings.last_folder = dlg.get_current_folder()
146 else:
147 filename = None
149 dlg.destroy()
150 return filename
152 class BookmarksWindow(gtk.Window):
153 def __init__(self):
154 gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
155 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
157 self.set_title('Bookmarks')
158 window_icon = util.find_image('panucci.png')
159 if window_icon is not None:
160 self.set_icon_from_file( window_icon )
162 self.set_default_size(400, 300)
163 self.set_border_width(10)
164 self.vbox = gtk.VBox()
165 self.vbox.set_spacing(5)
166 self.treeview = gtk.TreeView()
167 self.treeview.set_enable_tree_lines(True)
168 self.treeview.set_headers_visible(True)
169 self.update_model()
171 ncol = gtk.TreeViewColumn(_('Name'))
172 ncell = gtk.CellRendererText()
173 ncell.set_property('editable', True)
174 ncell.connect('edited', self.label_edited)
175 ncol.pack_start(ncell)
176 ncol.add_attribute(ncell, 'text', 1)
178 tcol = gtk.TreeViewColumn(_('Position'))
179 tcell = gtk.CellRendererText()
180 tcol.pack_start(tcell)
181 tcol.add_attribute(tcell, 'text', 2)
183 self.treeview.append_column(ncol)
184 self.treeview.append_column(tcol)
185 self.treeview.connect('drag-data-received', self.drag_data_recieved)
186 self.treeview.connect('drag_data_get', self.drag_data_get_data)
188 treeview_targets = [
189 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
191 self.treeview.enable_model_drag_source(
192 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
194 self.treeview.enable_model_drag_dest(
195 treeview_targets, gtk.gdk.ACTION_COPY )
197 sw = gtk.ScrolledWindow()
198 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
199 sw.set_shadow_type(gtk.SHADOW_IN)
200 sw.add(self.treeview)
201 self.vbox.add(sw)
202 self.hbox = gtk.HButtonBox()
203 self.add_button = gtk.Button(gtk.STOCK_ADD)
204 self.add_button.set_use_stock(True)
205 self.add_button.connect('clicked', self.add_bookmark)
206 self.hbox.pack_start(self.add_button)
207 self.remove_button = gtk.Button(gtk.STOCK_REMOVE)
208 self.remove_button.set_use_stock(True)
209 self.remove_button.connect('clicked', self.remove_bookmark)
210 self.hbox.pack_start(self.remove_button)
211 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
212 self.jump_button.set_use_stock(True)
213 self.jump_button.connect('clicked', self.jump_bookmark)
214 self.hbox.pack_start(self.jump_button)
215 self.close_button = gtk.Button(gtk.STOCK_CLOSE)
216 self.close_button.set_use_stock(True)
217 self.close_button.connect('clicked', self.close)
218 self.hbox.pack_start(self.close_button)
219 self.vbox.pack_start(self.hbox, False, True)
220 self.add(self.vbox)
221 self.show_all()
223 def drag_data_get_data(
224 self, treeview, context, selection, target_id, timestamp):
226 treeselection = treeview.get_selection()
227 model, iter = treeselection.get_selected()
228 # only allow moving around top-level parents
229 if model.iter_parent(iter) is None:
230 # send the path of the selected row
231 data = model.get_string_from_iter(iter)
232 selection.set(selection.target, 8, data)
233 else:
234 self.__log.debug("Can't move children...")
236 def drag_data_recieved(
237 self, treeview, context, x, y, selection, info, timestamp):
239 drop_info = treeview.get_dest_row_at_pos(x, y)
241 # TODO: If user drags the row past the last row, drop_info is None
242 # I'm not sure if it's safe to simply assume that None is
243 # euqivalent to the last row...
244 if None not in [ drop_info and selection.data ]:
245 model = treeview.get_model()
246 path, position = drop_info
248 from_iter = model.get_iter_from_string(selection.data)
250 # make sure the to_iter doesn't have a parent
251 to_iter = model.get_iter(path)
252 if model.iter_parent(to_iter) is not None:
253 to_iter = model.iter_parent(to_iter)
255 from_row = model.get_path(from_iter)[0]
256 to_row = path[0]
258 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
259 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
260 model.move_before( from_iter, to_iter )
261 to_row = to_row - 1 if from_row < to_row else to_row
262 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
263 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
264 model.move_after( from_iter, to_iter )
265 to_row = to_row + 1 if from_row > to_row else to_row
266 else:
267 self.__log.debug('Drop not supported: %s', position)
269 # don't do anything if we're not actually moving rows around
270 if from_row != to_row:
271 player.playlist.move_item( from_row, to_row )
273 else:
274 self.__log.debug('No drop_data or selection.data available')
276 def update_model(self):
277 self.model = player.playlist.get_bookmark_model()
278 self.treeview.set_model(self.model)
280 def close(self, w):
281 player.playlist.update_bookmarks()
282 self.destroy()
284 def label_edited(self, cellrenderer, path, new_text):
285 iter = self.model.get_iter(path)
286 old_text = self.model.get_value(iter, 1)
288 if new_text.strip():
289 if old_text != new_text:
290 self.model.set_value(iter, 1, new_text)
291 m, bkmk_id, biter, item_id, iiter = self.__cur_selection()
293 player.playlist.update_bookmark(
294 item_id, bkmk_id, name=new_text )
295 else:
296 self.model.set_value(iter, 1, old_text)
298 def add_bookmark(self, w=None, lbl=None, pos=None):
299 (label, position) = player.get_formatted_position(pos)
300 label = label if lbl is None else lbl
301 position = position if pos is None else pos
302 player.playlist.save_bookmark( label, position )
303 self.update_model()
305 def __cur_selection(self):
306 bookmark_id, bookmark_iter, item_id, item_iter = (None,)*4
308 selection = self.treeview.get_selection()
309 # Assume the user selects a bookmark.
310 # bookmark_iter will get set to None if that is not the case...
311 model, bookmark_iter = selection.get_selected()
313 if bookmark_iter is not None:
314 item_iter = model.iter_parent(bookmark_iter)
316 # bookmark_iter is actually an item_iter
317 if item_iter is None:
318 item_iter = bookmark_iter
319 item_id = model.get_value(item_iter, 0)
320 bookmark_id, bookmark_iter = None, None
321 else:
322 bookmark_id = model.get_value(bookmark_iter, 0)
323 item_id = model.get_value(item_iter, 0)
325 return model, bookmark_id, bookmark_iter, item_id, item_iter
327 def remove_bookmark(self, w):
328 model, bkmk_id, bkmk_iter, item_id, item_iter = self.__cur_selection()
329 player.playlist.remove_bookmark( item_id, bkmk_id )
330 if bkmk_iter is not None:
331 model.remove(bkmk_iter)
332 elif item_iter is not None:
333 model.remove(item_iter)
335 def jump_bookmark(self, w):
336 model, bkmk_id, bkmk_iter, item_id, item_iter = self.__cur_selection()
337 if item_iter is not None:
338 player.stop()
339 player.playlist.load_from_bookmark_id( item_id, bkmk_id )
340 player.play()
342 class GTK_Main(object):
344 def __init__(self, filename=None):
345 self.__log = logging.getLogger('panucci.panucci.GTK_Main')
346 interface.register_gui(self)
347 self.pickle_file_conversion()
349 self.recent_files = []
350 self.progress_timer_id = None
351 self.volume_timer_id = None
352 self.make_main_window()
353 self.has_coverart = False
354 self.set_volume(settings.volume)
356 if util.platform==util.MAEMO and interface.headset_device is not None:
357 # Enable play/pause with headset button
358 interface.headset_device.connect_to_signal(
359 'Condition', self.handle_headset_button )
361 player.register( 'stopped', self.on_player_stopped )
362 player.register( 'playing', self.on_player_playing )
363 player.register( 'paused', self.on_player_paused )
364 player.register( 'end_of_playlist', self.on_player_end_of_playlist )
365 player.playlist.register( 'new_track', self.on_player_new_track )
366 player.playlist.register( 'file_queued', self.on_file_queued )
367 player.init()
369 def make_main_window(self):
370 import pango
372 if util.platform == util.MAEMO:
373 self.app = hildon.Program()
374 window = hildon.Window()
375 self.app.add_window(window)
376 else:
377 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
379 window.set_title('Panucci')
380 self.window_icon = util.find_image('panucci.png')
381 if self.window_icon is not None:
382 window.set_icon_from_file( self.window_icon )
383 window.set_default_size(400, -1)
384 window.set_border_width(0)
385 window.connect("destroy", self.destroy)
386 self.main_window = window
388 if util.platform == util.MAEMO:
389 window.set_menu(self.create_menu())
390 else:
391 menu_vbox = gtk.VBox()
392 menu_vbox.set_spacing(0)
393 window.add(menu_vbox)
394 menu_bar = gtk.MenuBar()
395 root_menu = gtk.MenuItem('Panucci')
396 root_menu.set_submenu(self.create_menu())
397 menu_bar.append(root_menu)
398 menu_vbox.pack_start(menu_bar, False, False, 0)
399 menu_bar.show()
401 main_hbox = gtk.HBox()
402 main_hbox.set_spacing(6)
403 if util.platform == util.MAEMO:
404 window.add(main_hbox)
405 else:
406 menu_vbox.pack_end(main_hbox, True, True, 6)
408 main_vbox = gtk.VBox()
409 main_vbox.set_spacing(6)
410 # add a vbox to the main_hbox
411 main_hbox.pack_start(main_vbox, True, True)
413 # a hbox to hold the cover art and metadata vbox
414 metadata_hbox = gtk.HBox()
415 metadata_hbox.set_spacing(6)
416 main_vbox.pack_start(metadata_hbox, True, False)
418 self.cover_art = gtk.Image()
419 metadata_hbox.pack_start( self.cover_art, False, False )
421 # vbox to hold metadata
422 metadata_vbox = gtk.VBox()
423 metadata_vbox.set_spacing(8)
424 empty_label = gtk.Label()
425 metadata_vbox.pack_start(empty_label, True, True)
426 self.artist_label = gtk.Label('')
427 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
428 metadata_vbox.pack_start(self.artist_label, False, False)
429 self.album_label = gtk.Label('')
430 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
431 metadata_vbox.pack_start(self.album_label, False, False)
432 self.title_label = gtk.Label('')
433 self.title_label.set_line_wrap(True)
434 metadata_vbox.pack_start(self.title_label, False, False)
435 empty_label = gtk.Label()
436 metadata_vbox.pack_start(empty_label, True, True)
437 metadata_hbox.pack_start( metadata_vbox, True, True )
439 progress_eventbox = gtk.EventBox()
440 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
441 progress_eventbox.connect('button-press-event', self.on_progressbar_changed)
442 self.progress = gtk.ProgressBar()
443 # make the progress bar more "finger-friendly"
444 if util.platform == util.MAEMO:
445 self.progress.set_size_request( -1, 50 )
446 progress_eventbox.add(self.progress)
447 main_vbox.pack_start( progress_eventbox, False, False )
449 # make the button box
450 buttonbox = gtk.HBox()
451 self.rrewind_button = gtk.Button('')
452 image(self.rrewind_button, 'media-skip-backward.png')
453 self.rrewind_button.connect('clicked', self.seekbutton_callback, -1*long_seek)
454 buttonbox.add(self.rrewind_button)
455 self.rewind_button = gtk.Button('')
456 image(self.rewind_button, 'media-seek-backward.png')
457 self.rewind_button.connect('clicked', self.seekbutton_callback, -1*short_seek)
458 buttonbox.add(self.rewind_button)
459 self.play_pause_button = gtk.Button('')
460 image(self.play_pause_button, gtk.STOCK_OPEN, True)
461 self.button_handler_id = self.play_pause_button.connect(
462 'clicked', self.open_file_callback )
463 buttonbox.add(self.play_pause_button)
464 self.forward_button = gtk.Button('')
465 image(self.forward_button, 'media-seek-forward.png')
466 self.forward_button.connect('clicked', self.seekbutton_callback, short_seek)
467 buttonbox.add(self.forward_button)
468 self.fforward_button = gtk.Button('')
469 image(self.fforward_button, 'media-skip-forward.png')
470 self.fforward_button.connect('clicked', self.seekbutton_callback, long_seek)
471 buttonbox.add(self.fforward_button)
472 self.bookmarks_button = gtk.Button('')
473 image(self.bookmarks_button, 'bookmark-new.png')
474 self.bookmarks_button.connect('clicked', self.bookmarks_callback)
475 buttonbox.add(self.bookmarks_button)
476 self.set_controls_sensitivity(False)
477 main_vbox.pack_start(buttonbox, False, False)
479 window.show_all()
481 if util.platform == util.MAEMO:
482 self.volume = hildon.VVolumebar()
483 self.volume.set_property('can-focus', False)
484 self.volume.connect('level_changed', self.volume_changed_hildon)
485 self.volume.connect('mute_toggled', self.mute_toggled)
486 window.connect('key-press-event', self.on_key_press)
487 main_hbox.pack_start(self.volume, False, True)
489 # Add a button to pop out the volume bar
490 self.volume_button = gtk.ToggleButton('')
491 image(self.volume_button, 'media-speaker.png')
492 self.volume_button.connect('clicked', self.toggle_volumebar)
493 self.volume.connect(
494 'show', lambda x: self.volume_button.set_active(True))
495 self.volume.connect(
496 'hide', lambda x: self.volume_button.set_active(False))
497 buttonbox.add(self.volume_button)
498 self.volume_button.show()
500 # Disable focus for all widgets, so we can use the cursor
501 # keys + enter to directly control our media player, which
502 # is handled by "key-press-event"
503 for w in (
504 self.rrewind_button, self.rewind_button,
505 self.play_pause_button, self.forward_button,
506 self.fforward_button, self.progress,
507 self.bookmarks_button, self.volume_button, ):
508 w.unset_flags(gtk.CAN_FOCUS)
509 else:
510 self.volume = gtk.VolumeButton()
511 self.volume.connect('value-changed', self.volume_changed_gtk)
512 buttonbox.add(self.volume)
513 self.volume.show()
515 def create_menu(self):
516 # the main menu
517 menu = gtk.Menu()
519 menu_open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
520 menu_open.connect("activate", self.open_file_callback)
521 menu.append(menu_open)
523 menu_queue = gtk.MenuItem(_('Add file to the queue'))
524 menu_queue.connect('activate', self.queue_file_callback)
525 menu.append(menu_queue)
527 # the recent files menu
528 self.menu_recent = gtk.MenuItem(_('Recent Files'))
529 menu.append(self.menu_recent)
530 self.create_recent_files_menu()
532 menu.append(gtk.SeparatorMenuItem())
534 menu_bookmarks = gtk.MenuItem(_('Bookmarks'))
535 menu_bookmarks.connect('activate', self.bookmarks_callback)
536 menu.append(menu_bookmarks)
539 # the settings sub-menu
540 menu_settings = gtk.MenuItem(_('Settings'))
541 menu.append(menu_settings)
543 menu_settings_sub = gtk.Menu()
544 menu_settings.set_submenu(menu_settings_sub)
546 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
547 menu_settings_lock_progress.connect('toggled', lambda w:
548 setattr( settings, 'progress_locked', w.get_active()))
549 menu_settings_lock_progress.set_active(self.lock_progress)
550 menu_settings_sub.append(menu_settings_lock_progress)
552 menu.append(gtk.SeparatorMenuItem())
554 # the donate sub-menu
555 menu_donate = gtk.MenuItem(_('Donate'))
556 menu.append(menu_donate)
558 menu_donate_sub = gtk.Menu()
559 menu_donate.set_submenu(menu_donate_sub)
561 menu_donate_device = gtk.MenuItem(_('Developer device'))
562 menu_donate_device.connect("activate", lambda w: webbrowser.open_new(donate_device_url))
563 menu_donate_sub.append(menu_donate_device)
565 menu_donate_wishlist = gtk.MenuItem(_('Amazon Wishlist'))
566 menu_donate_wishlist.connect("activate", lambda w: webbrowser.open_new(donate_wishlist_url))
567 menu_donate_sub.append(menu_donate_wishlist)
569 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
570 menu_about.connect("activate", self.show_about, self.main_window)
571 menu.append(menu_about)
573 menu.append(gtk.SeparatorMenuItem())
575 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
576 menu_quit.connect("activate", self.destroy)
577 menu.append(menu_quit)
579 return menu
581 def create_recent_files_menu( self ):
582 max_files = settings.max_recent_files
583 self.recent_files = player.playlist.get_recent_files(max_files)
584 menu_recent_sub = gtk.Menu()
586 temp_playlist = os.path.expanduser(settings.temp_playlist)
588 if len(self.recent_files) > 0:
589 for f in self.recent_files:
590 # don't include the temporary playlist in the file list
591 if f == temp_playlist: continue
592 filename, extension = os.path.splitext(os.path.basename(f))
593 menu_item = gtk.MenuItem( filename.replace('_', ' '))
594 menu_item.connect('activate', self.on_recent_file_activate, f)
595 menu_recent_sub.append(menu_item)
596 else:
597 menu_item = gtk.MenuItem(_('No recent files available.'))
598 menu_item.set_sensitive(False)
599 menu_recent_sub.append(menu_item)
601 self.menu_recent.set_submenu(menu_recent_sub)
603 def on_recent_file_activate(self, widget, filepath):
604 self.play_file(filepath)
606 @property
607 def lock_progress(self):
608 return settings.progress_locked
610 def show_about(self, w, win):
611 dialog = gtk.AboutDialog()
612 dialog.set_website(about_website)
613 dialog.set_website_label(about_website)
614 dialog.set_name(about_name)
615 dialog.set_authors(about_authors)
616 dialog.set_comments(about_text)
617 dialog.set_version(app_version)
618 dialog.run()
619 dialog.destroy()
621 def destroy(self, widget):
622 player.quit()
623 gtk.main_quit()
625 def handle_headset_button(self, event, button):
626 if event == 'ButtonPressed' and button == 'phone':
627 self.on_btn_play_pause_clicked()
629 def queue_file_callback(self, widget=None):
630 filename = get_file_from_filechooser(self.main_window)
631 if filename is not None:
632 player.playlist.append(filename)
634 def check_queue(self):
635 """ Makes sure the queue is saved if it has been modified
636 True means a new file can be opened
637 False means the user does not want to continue """
639 if player.playlist.queue_modified:
640 response = dialog(
641 self.main_window, _('Save queue to playlist file'),
642 _('Save Queue?'), _("The queue has been modified, "
643 "you will lose all additions if you don't save.") )
645 self.__log.debug('Response to "Save Queue?": %s', response)
647 if response is None:
648 return False
649 elif response:
650 return self.save_to_playlist_callback()
651 elif not response:
652 return True
653 else:
654 return False
655 else:
656 return True
658 def open_file_callback(self, widget=None):
659 if self.check_queue():
660 filename = get_file_from_filechooser(self.main_window)
661 if filename is not None:
662 self._play_file(filename)
664 def save_to_playlist_callback(self, widget=None):
665 filename = get_file_from_filechooser(
666 self.main_window, save_file=True, save_to='playlist.m3u' )
668 if filename is None:
669 return False
671 if os.path.isfile(filename):
672 response = dialog(
673 self.main_window, _('Overwrite File Warning'),
674 _('Overwrite ') + '%s?' % os.path.basename(filename),
675 _('All data in the file will be erased.') )
677 if response is None:
678 return None
679 elif response:
680 pass
681 elif not response:
682 return self.save_to_playlist_callback()
684 ext = util.detect_filetype(filename)
685 if not player.playlist.save_to_new_playlist(filename, ext):
686 util.notify(_('Error saving playlist...'))
687 return False
689 return True
691 def set_controls_sensitivity(self, sensitive):
692 self.forward_button.set_sensitive(sensitive)
693 self.rewind_button.set_sensitive(sensitive)
694 self.fforward_button.set_sensitive(sensitive)
695 self.rrewind_button.set_sensitive(sensitive)
697 def on_key_press(self, widget, event):
698 if event.keyval == gtk.keysyms.F7: #plus
699 self.set_volume( min( 1, self.get_volume() + 0.10 ))
700 elif event.keyval == gtk.keysyms.F8: #minus
701 self.set_volume( max( 0, self.get_volume() - 0.10 ))
702 elif event.keyval == gtk.keysyms.Left: # seek back
703 self.rewind_callback(self.rewind_button)
704 elif event.keyval == gtk.keysyms.Right: # seek forward
705 self.forward_callback(self.forward_button)
706 elif event.keyval == gtk.keysyms.Return: # play/pause
707 self.on_btn_play_pause_clicked()
709 # The following two functions get and set the
710 # volume from the volume control widgets.
711 def get_volume(self):
712 if util.platform == util.MAEMO:
713 return self.volume.get_level()/100.0
714 else:
715 return self.volume.get_value()
717 def set_volume(self, vol):
718 """ vol is a float from 0 to 1 """
719 assert 0 <= vol <= 1
720 if util.platform == util.MAEMO:
721 self.volume.set_level(vol*100.0)
722 else:
723 self.volume.set_value(vol)
725 def __set_volume_hide_timer(self, timeout, force_show=False):
726 if force_show or self.volume_button.get_active():
727 self.volume.show()
728 if self.volume_timer_id is not None:
729 gobject.source_remove(self.volume_timer_id)
731 self.volume_timer_id = gobject.timeout_add(
732 1000 * timeout, self.__volume_hide_callback )
734 def __volume_hide_callback(self):
735 self.volume_timer_id = None
736 self.volume.hide()
737 return False
739 def toggle_volumebar(self, widget=None):
740 if self.volume_timer_id is None:
741 self.__set_volume_hide_timer(5)
742 else:
743 self.__volume_hide_callback()
745 def volume_changed_gtk(self, widget, new_value=0.5):
746 player.volume_level = new_value
748 def volume_changed_hildon(self, widget):
749 self.__set_volume_hide_timer( 4, force_show=True )
750 player.volume_level = widget.get_level()/100.0
752 def mute_toggled(self, widget):
753 if widget.get_mute():
754 player.volume_level = 0
755 else:
756 player.volume_level = widget.get_level()/100.0
758 def show_main_window(self):
759 self.main_window.present()
761 def play_file(self, filename):
762 if self.check_queue():
763 self._play_file(filename)
765 def _play_file(self, filename, pause_on_load=False):
766 player.stop()
768 player.playlist.load( os.path.abspath(filename) )
769 if player.playlist.is_empty:
770 return False
772 player.play()
774 def on_player_stopped(self):
775 self.stop_progress_timer()
776 self.title_label.set_size_request(-1,-1)
777 self.reset_progress()
778 self.set_controls_sensitivity(False)
780 def on_player_playing(self):
781 self.start_progress_timer()
782 image(self.play_pause_button, 'media-playback-pause.png')
784 def on_player_new_track(self, metadata):
785 image(self.play_pause_button, 'media-playback-start.png')
786 self.play_pause_button.disconnect(self.button_handler_id)
787 self.button_handler_id = self.play_pause_button.connect(
788 'clicked', self.on_btn_play_pause_clicked )
790 self.set_controls_sensitivity(True)
791 for widget in [self.title_label,self.artist_label,self.album_label]:
792 widget.set_text('')
793 widget.hide()
795 self.cover_art.hide()
796 self.has_coverart = False
797 self.set_metadata(metadata)
799 text, position = player.get_formatted_position()
800 estimated_length = metadata['length']
801 self.set_progress_callback( position, estimated_length )
803 def on_player_paused(self):
804 self.stop_progress_timer() # This should save some power
805 image(self.play_pause_button, 'media-playback-start.png')
807 def on_player_end_of_playlist(self):
808 self.play_pause_button.disconnect(self.button_handler_id)
809 self.button_handler_id = self.play_pause_button.connect(
810 'clicked', self.open_file_callback )
811 image(self.play_pause_button, gtk.STOCK_OPEN, True)
813 def on_file_queued(self, filepath, success):
814 filename = os.path.basename(filepath)
815 if success:
816 self.__log.info(util.notify('%s added successfully.' % filename ))
817 else:
818 self.__log.error(
819 util.notify('Error adding %s to the queue.' % filename) )
821 def reset_progress(self):
822 self.progress.set_fraction(0)
823 self.set_progress_callback(0,0)
825 def set_progress_callback(self, time_elapsed, total_time):
826 """ times must be in nanoseconds """
827 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
828 util.convert_ns(total_time) )
829 self.progress.set_text( time_string )
830 fraction = float(time_elapsed) / float(total_time) if total_time else 0
831 self.progress.set_fraction( fraction )
833 def on_progressbar_changed(self, widget, event):
834 if ( not self.lock_progress and
835 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
836 new_fraction = event.x/float(widget.get_allocation().width)
837 resp = player.do_seek(percent=new_fraction)
838 if resp:
839 # Preemptively update the progressbar to make seeking smoother
840 self.set_progress_callback( *resp )
842 def on_btn_play_pause_clicked(self, widget=None):
843 player.play_pause_toggle()
845 def progress_timer_callback( self ):
846 if player.playing and not player.seeking:
847 pos_int, dur_int = player.get_position_duration()
848 # This prevents bogus values from being set while seeking
849 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
850 self.set_progress_callback( pos_int, dur_int )
851 return True
853 def start_progress_timer( self ):
854 if self.progress_timer_id is not None:
855 self.stop_progress_timer()
857 self.progress_timer_id = gobject.timeout_add(
858 1000, self.progress_timer_callback )
860 def stop_progress_timer( self ):
861 if self.progress_timer_id is not None:
862 gobject.source_remove( self.progress_timer_id )
863 self.progress_timer_id = None
865 def set_coverart( self, pixbuf ):
866 self.cover_art.set_from_pixbuf(pixbuf)
867 self.cover_art.show()
868 self.has_coverart = True
870 def set_metadata( self, tag_message ):
871 tags = { 'title': self.title_label, 'artist': self.artist_label,
872 'album': self.album_label }
874 if tag_message.has_key('image') and tag_message['image'] is not None:
875 value = tag_message['image']
877 pbl = gtk.gdk.PixbufLoader()
878 try:
879 pbl.write(value)
880 pbl.close()
881 pixbuf = pbl.get_pixbuf().scale_simple(
882 coverart_size[0], coverart_size[1], gtk.gdk.INTERP_BILINEAR )
883 self.set_coverart(pixbuf)
884 except Exception, e:
885 self.__log.exception('Error setting coverart...')
887 tag_vals = dict([ (i,'') for i in tags.keys()])
888 for tag,value in tag_message.iteritems():
889 if tags.has_key(tag) and value is not None and value.strip():
890 tags[tag].set_markup('<big>'+value+'</big>')
891 tag_vals[tag] = value
892 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
893 tags[tag].show()
894 if tag == 'title':
895 if util.platform == util.MAEMO:
896 self.main_window.set_title(value)
897 # oh man this is hacky :(
898 if self.has_coverart:
899 tags[tag].set_size_request(420,-1)
900 if len(value) >= 80: value = value[:80] + '...'
901 else:
902 self.main_window.set_title('Panucci - ' + value)
904 tags[tag].set_markup('<b><big>'+value+'</big></b>')
906 def seekbutton_callback( self, widget, seek_amount ):
907 resp = player.do_seek(from_current=seek_amount*10**9)
908 if resp:
909 # Preemptively update the progressbar to make seeking smoother
910 self.set_progress_callback( *resp )
912 def bookmarks_callback(self, w):
913 BookmarksWindow()
915 def pickle_file_conversion(self):
916 pickle_file = os.path.expanduser('~/.rmp-bookmarks')
917 if os.path.isfile(pickle_file):
918 import pickle_converter
920 self.__log.info(
921 util.notify( _('Converting old pickle format to SQLite.') ))
922 self.__log.info( util.notify( _('This may take a while...') ))
924 if pickle_converter.load_pickle_file(pickle_file):
925 self.__log.info(
926 util.notify( _('Pickle file converted successfully.') ))
927 else:
928 self.__log.error( util.notify(
929 _('Error converting pickle file, check your log...') ))
931 def run(filename=None):
932 GTK_Main( filename )
933 gtk.main()
935 if __name__ == '__main__':
936 log.error( 'WARNING: Use the "panucci" executable to run this program.' )
937 log.error( 'Exiting...' )
938 sys.exit(1)