Make the playlist tab work
[panucci.git] / src / panucci / panucci.py
blobf4292cffc339f7a3a9b4c7921d53babab683c8ed
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 = [200, 200] if util.platform == util.MAEMO else [110, 110]
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(
114 toplevel_window, folder=False, save_file=False, save_to=None):
116 if folder:
117 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
118 else:
119 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
121 if util.platform == util.MAEMO:
122 if save_file:
123 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
124 else:
125 args = ( toplevel_window, open_action )
127 dlg = hildon.FileChooserDialog( *args )
128 else:
129 if save_file:
130 args = ( _('Select file to save playlist to'), None,
131 gtk.FILE_CHOOSER_ACTION_SAVE,
132 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
133 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
134 else:
135 args = ( _('Select podcast or audiobook'), None, open_action,
136 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
137 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
139 dlg = gtk.FileChooserDialog(*args)
141 current_folder = os.path.expanduser(settings.last_folder)
143 if current_folder is not None and os.path.isdir(current_folder):
144 dlg.set_current_folder(current_folder)
146 if save_file and save_to is not None:
147 dlg.set_current_name(save_to)
149 if dlg.run() == gtk.RESPONSE_OK:
150 filename = dlg.get_filename()
151 settings.last_folder = dlg.get_current_folder()
152 else:
153 filename = None
155 dlg.destroy()
156 return filename
158 def set_stock_button_text( button, text ):
159 alignment = button.get_child()
160 hbox = alignment.get_child()
161 image, label = hbox.get_children()
162 label.set_text(text)
164 class PlaylistTab(gtk.VBox):
165 def __init__(self, main_window):
166 gtk.VBox.__init__(self)
167 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
168 self.main = main_window
170 self.set_spacing(5)
171 self.treeview = gtk.TreeView()
172 self.treeview.set_headers_visible(True)
174 # The tree lines look nasty on maemo
175 if util.platform == util.LINUX:
176 self.treeview.set_enable_tree_lines(True)
177 self.update_model()
179 ncol = gtk.TreeViewColumn(_('Name'))
180 ncell = gtk.CellRendererText()
181 ncell.set_property('editable', True)
182 ncell.connect('edited', self.label_edited)
183 ncol.pack_start(ncell)
184 ncol.add_attribute(ncell, 'text', 1)
186 tcol = gtk.TreeViewColumn(_('Position'))
187 tcell = gtk.CellRendererText()
188 tcol.pack_start(tcell)
189 tcol.add_attribute(tcell, 'text', 2)
191 self.treeview.append_column(ncol)
192 self.treeview.append_column(tcol)
193 self.treeview.connect('drag-data-received', self.drag_data_recieved)
194 self.treeview.connect('drag_data_get', self.drag_data_get_data)
196 treeview_targets = [
197 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
199 self.treeview.enable_model_drag_source(
200 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
202 self.treeview.enable_model_drag_dest(
203 treeview_targets, gtk.gdk.ACTION_COPY )
205 sw = gtk.ScrolledWindow()
206 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
207 sw.set_shadow_type(gtk.SHADOW_IN)
208 sw.add(self.treeview)
209 self.add(sw)
211 self.hbox = gtk.HBox()
213 self.add_button = gtk.Button(gtk.STOCK_NEW)
214 self.add_button.set_use_stock(True)
215 set_stock_button_text( self.add_button, _('Add File') )
216 self.add_button.connect('clicked', self.add_file)
217 self.hbox.pack_start(self.add_button, True, False)
219 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
220 self.dir_button.set_use_stock(True)
221 set_stock_button_text( self.dir_button, _('Add Directory') )
222 self.dir_button.connect('clicked', self.add_directory)
223 self.hbox.pack_start(self.dir_button, True, False)
225 self.remove_button = gtk.Button(gtk.STOCK_REMOVE)
226 self.remove_button.set_use_stock(True)
227 self.remove_button.connect('clicked', self.remove_bookmark)
228 self.hbox.pack_start(self.remove_button, True, False)
230 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
231 self.jump_button.set_use_stock(True)
232 self.jump_button.connect('clicked', self.jump_bookmark)
233 self.hbox.pack_start(self.jump_button, True, False)
234 self.pack_start(self.hbox, False, True)
236 player.playlist.register(
237 'file_queued', lambda x,y,z: self.update_model() )
239 self.show_all()
241 def drag_data_get_data(
242 self, treeview, context, selection, target_id, timestamp):
244 treeselection = treeview.get_selection()
245 model, iter = treeselection.get_selected()
246 # only allow moving around top-level parents
247 if model.iter_parent(iter) is None:
248 # send the path of the selected row
249 data = model.get_string_from_iter(iter)
250 selection.set(selection.target, 8, data)
251 else:
252 self.__log.debug("Can't move children...")
254 def drag_data_recieved(
255 self, treeview, context, x, y, selection, info, timestamp):
257 drop_info = treeview.get_dest_row_at_pos(x, y)
259 # TODO: If user drags the row past the last row, drop_info is None
260 # I'm not sure if it's safe to simply assume that None is
261 # euqivalent to the last row...
262 if None not in [ drop_info and selection.data ]:
263 model = treeview.get_model()
264 path, position = drop_info
266 from_iter = model.get_iter_from_string(selection.data)
268 # make sure the to_iter doesn't have a parent
269 to_iter = model.get_iter(path)
270 if model.iter_parent(to_iter) is not None:
271 to_iter = model.iter_parent(to_iter)
273 from_row = model.get_path(from_iter)[0]
274 to_row = path[0]
276 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
277 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
278 model.move_before( from_iter, to_iter )
279 to_row = to_row - 1 if from_row < to_row else to_row
280 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
281 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
282 model.move_after( from_iter, to_iter )
283 to_row = to_row + 1 if from_row > to_row else to_row
284 else:
285 self.__log.debug('Drop not supported: %s', position)
287 # don't do anything if we're not actually moving rows around
288 if from_row != to_row:
289 player.playlist.move_item( from_row, to_row )
291 else:
292 self.__log.debug('No drop_data or selection.data available')
294 def update_model(self):
295 self.model = player.playlist.get_bookmark_model()
296 self.treeview.set_model(self.model)
297 self.treeview.expand_all()
299 def label_edited(self, cellrenderer, path, new_text):
300 iter = self.model.get_iter(path)
301 old_text = self.model.get_value(iter, 1)
303 if new_text.strip():
304 if old_text != new_text:
305 self.model.set_value(iter, 1, new_text)
306 m, bkmk_id, biter, item_id, iiter = self.__cur_selection()
308 player.playlist.update_bookmark(
309 item_id, bkmk_id, name=new_text )
310 else:
311 self.model.set_value(iter, 1, old_text)
313 def add_bookmark(self, w=None, lbl=None, pos=None):
314 (label, position) = player.get_formatted_position(pos)
315 label = label if lbl is None else lbl
316 position = position if pos is None else pos
317 player.playlist.save_bookmark( label, position )
318 util.notify(_('Bookmark Added.'))
319 self.update_model()
321 def add_file(self, widget):
322 filename = get_file_from_filechooser(self.main.main_window)
323 if filename is not None:
324 player.playlist.append(filename)
326 def add_directory(self, widget):
327 directory = get_file_from_filechooser(
328 self.main.main_window, folder=True )
329 if directory is not None:
330 player.playlist.load_directory(directory, append=True)
332 def __cur_selection(self):
333 bookmark_id, bookmark_iter, item_id, item_iter = (None,)*4
335 selection = self.treeview.get_selection()
336 # Assume the user selects a bookmark.
337 # bookmark_iter will get set to None if that is not the case...
338 model, bookmark_iter = selection.get_selected()
340 if bookmark_iter is not None:
341 item_iter = model.iter_parent(bookmark_iter)
343 # bookmark_iter is actually an item_iter
344 if item_iter is None:
345 item_iter = bookmark_iter
346 item_id = model.get_value(item_iter, 0)
347 bookmark_id, bookmark_iter = None, None
348 else:
349 bookmark_id = model.get_value(bookmark_iter, 0)
350 item_id = model.get_value(item_iter, 0)
352 return model, bookmark_id, bookmark_iter, item_id, item_iter
354 def remove_bookmark(self, w):
355 model, bkmk_id, bkmk_iter, item_id, item_iter = self.__cur_selection()
356 player.playlist.remove_bookmark( item_id, bkmk_id )
357 if bkmk_iter is not None:
358 model.remove(bkmk_iter)
359 elif item_iter is not None:
360 model.remove(item_iter)
362 def jump_bookmark(self, w):
363 model, bkmk_id, bkmk_iter, item_id, item_iter = self.__cur_selection()
364 if item_iter is not None:
365 player.playlist.load_from_bookmark_id( item_id, bkmk_id )
367 # FIXME: The player/playlist should be able to take care of this
368 if not player.playing:
369 player.play()
371 class GTK_Main(object):
373 def __init__(self, filename=None):
374 self.__log = logging.getLogger('panucci.panucci.GTK_Main')
375 interface.register_gui(self)
376 self.pickle_file_conversion()
378 self.recent_files = []
379 self.progress_timer_id = None
380 self.volume_timer_id = None
381 self.make_main_window()
382 self.has_coverart = False
383 self.set_volume(settings.volume)
385 if util.platform==util.MAEMO and interface.headset_device is not None:
386 # Enable play/pause with headset button
387 interface.headset_device.connect_to_signal(
388 'Condition', self.handle_headset_button )
390 player.register( 'stopped', self.on_player_stopped )
391 player.register( 'playing', self.on_player_playing )
392 player.register( 'paused', self.on_player_paused )
393 player.register( 'end_of_playlist', self.on_player_end_of_playlist )
394 player.playlist.register('new_track_metadata',self.on_player_new_track)
395 player.playlist.register( 'file_queued', self.on_file_queued )
396 player.init(filepath=filename)
398 def make_main_window(self):
399 import pango
401 if util.platform == util.MAEMO:
402 self.app = hildon.Program()
403 window = hildon.Window()
404 self.app.add_window(window)
405 else:
406 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
408 window.set_title('Panucci')
409 self.window_icon = util.find_image('panucci.png')
410 if self.window_icon is not None:
411 window.set_icon_from_file( self.window_icon )
412 window.set_default_size(400, -1)
413 window.set_border_width(0)
414 window.connect("destroy", self.destroy)
415 self.main_window = window
417 if util.platform == util.MAEMO:
418 window.set_menu(self.create_menu())
419 else:
420 menu_vbox = gtk.VBox()
421 menu_vbox.set_spacing(0)
422 window.add(menu_vbox)
423 menu_bar = gtk.MenuBar()
424 root_menu = gtk.MenuItem('Panucci')
425 root_menu.set_submenu(self.create_menu())
426 menu_bar.append(root_menu)
427 menu_vbox.pack_start(menu_bar, False, False, 0)
428 menu_bar.show()
430 self.notebook = gtk.Notebook()
432 if util.platform == util.MAEMO:
433 window.add(self.notebook)
434 else:
435 menu_vbox.pack_end(self.notebook, True, True, 6)
437 main_hbox = gtk.HBox()
438 self.notebook.append_page(main_hbox, gtk.Label(_('Player')))
439 self.notebook.set_tab_label_packing(main_hbox,True,True,gtk.PACK_START)
441 main_vbox = gtk.VBox()
442 main_vbox.set_spacing(6)
443 # add a vbox to the main_hbox
444 main_hbox.pack_start(main_vbox, True, True)
446 # a hbox to hold the cover art and metadata vbox
447 metadata_hbox = gtk.HBox()
448 metadata_hbox.set_spacing(6)
449 main_vbox.pack_start(metadata_hbox, True, False)
451 self.cover_art = gtk.Image()
452 metadata_hbox.pack_start( self.cover_art, False, False )
454 # vbox to hold metadata
455 metadata_vbox = gtk.VBox()
456 metadata_vbox.set_spacing(8)
457 empty_label = gtk.Label()
458 metadata_vbox.pack_start(empty_label, True, True)
459 self.artist_label = gtk.Label('')
460 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
461 metadata_vbox.pack_start(self.artist_label, False, False)
462 self.album_label = gtk.Label('')
463 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
464 metadata_vbox.pack_start(self.album_label, False, False)
465 self.title_label = gtk.Label('')
466 self.title_label.set_line_wrap(True)
467 metadata_vbox.pack_start(self.title_label, False, False)
468 empty_label = gtk.Label()
469 metadata_vbox.pack_start(empty_label, True, True)
470 metadata_hbox.pack_start( metadata_vbox, True, True )
472 progress_eventbox = gtk.EventBox()
473 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
474 progress_eventbox.connect(
475 'button-press-event', self.on_progressbar_changed )
476 self.progress = gtk.ProgressBar()
477 # make the progress bar more "finger-friendly"
478 if util.platform == util.MAEMO:
479 self.progress.set_size_request( -1, 50 )
480 progress_eventbox.add(self.progress)
481 main_vbox.pack_start( progress_eventbox, False, False )
483 # make the button box
484 buttonbox = gtk.HBox()
485 self.rrewind_button = gtk.Button('')
486 image(self.rrewind_button, 'media-skip-backward.png')
487 self.rrewind_button.connect(
488 'clicked', self.seekbutton_callback, -1*long_seek )
489 buttonbox.add(self.rrewind_button)
490 self.rewind_button = gtk.Button('')
491 image(self.rewind_button, 'media-seek-backward.png')
492 self.rewind_button.connect(
493 'clicked', self.seekbutton_callback, -1*short_seek )
494 buttonbox.add(self.rewind_button)
495 self.play_pause_button = gtk.Button('')
496 image(self.play_pause_button, gtk.STOCK_OPEN, True)
497 self.button_handler_id = self.play_pause_button.connect(
498 'clicked', self.open_file_callback )
499 buttonbox.add(self.play_pause_button)
500 self.forward_button = gtk.Button('')
501 image(self.forward_button, 'media-seek-forward.png')
502 self.forward_button.connect(
503 'clicked', self.seekbutton_callback, short_seek )
504 buttonbox.add(self.forward_button)
505 self.fforward_button = gtk.Button('')
506 image(self.fforward_button, 'media-skip-forward.png')
507 self.fforward_button.connect(
508 'clicked', self.seekbutton_callback, long_seek )
509 buttonbox.add(self.fforward_button)
510 self.bookmarks_button = gtk.Button('')
511 image(self.bookmarks_button, 'bookmark-new.png')
512 buttonbox.add(self.bookmarks_button)
513 self.set_controls_sensitivity(False)
514 main_vbox.pack_start(buttonbox, False, False)
516 self.playlist_tab = PlaylistTab(self)
517 self.bookmarks_button.connect('clicked',self.playlist_tab.add_bookmark)
518 self.notebook.append_page(self.playlist_tab, gtk.Label(_('Playlist')))
519 self.notebook.set_tab_label_packing(
520 self.playlist_tab, True, True, gtk.PACK_START )
522 window.show_all()
523 self.notebook.set_current_page(0)
525 if util.platform == util.MAEMO:
526 self.volume = hildon.VVolumebar()
527 self.volume.set_property('can-focus', False)
528 self.volume.connect('level_changed', self.volume_changed_hildon)
529 self.volume.connect('mute_toggled', self.mute_toggled)
530 window.connect('key-press-event', self.on_key_press)
531 main_hbox.pack_start(self.volume, False, True)
533 # Add a button to pop out the volume bar
534 self.volume_button = gtk.ToggleButton('')
535 image(self.volume_button, 'media-speaker.png')
536 self.volume_button.connect('clicked', self.toggle_volumebar)
537 self.volume.connect(
538 'show', lambda x: self.volume_button.set_active(True))
539 self.volume.connect(
540 'hide', lambda x: self.volume_button.set_active(False))
541 buttonbox.add(self.volume_button)
542 self.volume_button.show()
544 # Disable focus for all widgets, so we can use the cursor
545 # keys + enter to directly control our media player, which
546 # is handled by "key-press-event"
547 for w in (
548 self.rrewind_button, self.rewind_button,
549 self.play_pause_button, self.forward_button,
550 self.fforward_button, self.progress,
551 self.bookmarks_button, self.volume_button, ):
552 w.unset_flags(gtk.CAN_FOCUS)
553 else:
554 self.volume = gtk.VolumeButton()
555 self.volume.connect('value-changed', self.volume_changed_gtk)
556 buttonbox.add(self.volume)
557 self.volume.show()
559 def create_menu(self):
560 # the main menu
561 menu = gtk.Menu()
563 menu_open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
564 menu_open.connect("activate", self.open_file_callback)
565 menu.append(menu_open)
567 # the recent files menu
568 self.menu_recent = gtk.MenuItem(_('Recent Files'))
569 menu.append(self.menu_recent)
570 self.create_recent_files_menu()
572 # the settings sub-menu
573 menu_settings = gtk.MenuItem(_('Settings'))
574 menu.append(menu_settings)
576 menu_settings_sub = gtk.Menu()
577 menu_settings.set_submenu(menu_settings_sub)
579 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
580 menu_settings_lock_progress.connect('toggled', lambda w:
581 setattr( settings, 'progress_locked', w.get_active()))
582 menu_settings_lock_progress.set_active(self.lock_progress)
583 menu_settings_sub.append(menu_settings_lock_progress)
585 menu.append(gtk.SeparatorMenuItem())
587 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
588 menu_about.connect("activate", self.show_about, self.main_window)
589 menu.append(menu_about)
591 menu.append(gtk.SeparatorMenuItem())
593 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
594 menu_quit.connect("activate", self.destroy)
595 menu.append(menu_quit)
597 return menu
599 def create_recent_files_menu( self ):
600 max_files = settings.max_recent_files
601 self.recent_files = player.playlist.get_recent_files(max_files)
602 menu_recent_sub = gtk.Menu()
604 temp_playlist = os.path.expanduser(settings.temp_playlist)
606 if len(self.recent_files) > 0:
607 for f in self.recent_files:
608 # don't include the temporary playlist in the file list
609 if f == temp_playlist: continue
610 filename, extension = os.path.splitext(os.path.basename(f))
611 menu_item = gtk.MenuItem( filename.replace('_', ' '))
612 menu_item.connect('activate', self.on_recent_file_activate, f)
613 menu_recent_sub.append(menu_item)
614 else:
615 menu_item = gtk.MenuItem(_('No recent files available.'))
616 menu_item.set_sensitive(False)
617 menu_recent_sub.append(menu_item)
619 self.menu_recent.set_submenu(menu_recent_sub)
621 def on_recent_file_activate(self, widget, filepath):
622 self.play_file(filepath)
624 @property
625 def lock_progress(self):
626 return settings.progress_locked
628 def show_about(self, w, win):
629 dialog = gtk.AboutDialog()
630 dialog.set_website(about_website)
631 dialog.set_website_label(about_website)
632 dialog.set_name(about_name)
633 dialog.set_authors(about_authors)
634 dialog.set_comments(about_text)
635 dialog.set_version(app_version)
636 dialog.run()
637 dialog.destroy()
639 def destroy(self, widget):
640 player.quit()
641 gtk.main_quit()
643 def handle_headset_button(self, event, button):
644 if event == 'ButtonPressed' and button == 'phone':
645 self.on_btn_play_pause_clicked()
647 def check_queue(self):
648 """ Makes sure the queue is saved if it has been modified
649 True means a new file can be opened
650 False means the user does not want to continue """
652 if player.playlist.queue_modified:
653 response = dialog(
654 self.main_window, _('Save queue to playlist file'),
655 _('Save Queue?'), _("The queue has been modified, "
656 "you will lose all additions if you don't save.") )
658 self.__log.debug('Response to "Save Queue?": %s', response)
660 if response is None:
661 return False
662 elif response:
663 return self.save_to_playlist_callback()
664 elif not response:
665 return True
666 else:
667 return False
668 else:
669 return True
671 def open_file_callback(self, widget=None):
672 if self.check_queue():
673 filename = get_file_from_filechooser(self.main_window)
674 if filename is not None:
675 self._play_file(filename)
677 def save_to_playlist_callback(self, widget=None):
678 filename = get_file_from_filechooser(
679 self.main_window, save_file=True, save_to='playlist.m3u' )
681 if filename is None:
682 return False
684 if os.path.isfile(filename):
685 response = dialog(
686 self.main_window, _('Overwrite File Warning'),
687 _('Overwrite ') + '%s?' % os.path.basename(filename),
688 _('All data in the file will be erased.') )
690 if response is None:
691 return None
692 elif response:
693 pass
694 elif not response:
695 return self.save_to_playlist_callback()
697 ext = util.detect_filetype(filename)
698 if not player.playlist.save_to_new_playlist(filename, ext):
699 util.notify(_('Error saving playlist...'))
700 return False
702 return True
704 def set_controls_sensitivity(self, sensitive):
705 self.forward_button.set_sensitive(sensitive)
706 self.rewind_button.set_sensitive(sensitive)
707 self.fforward_button.set_sensitive(sensitive)
708 self.rrewind_button.set_sensitive(sensitive)
710 def on_key_press(self, widget, event):
711 if event.keyval == gtk.keysyms.F7: #plus
712 self.set_volume( min( 1, self.get_volume() + 0.10 ))
713 elif event.keyval == gtk.keysyms.F8: #minus
714 self.set_volume( max( 0, self.get_volume() - 0.10 ))
715 elif event.keyval == gtk.keysyms.Left: # seek back
716 self.rewind_callback(self.rewind_button)
717 elif event.keyval == gtk.keysyms.Right: # seek forward
718 self.forward_callback(self.forward_button)
719 elif event.keyval == gtk.keysyms.Return: # play/pause
720 self.on_btn_play_pause_clicked()
722 # The following two functions get and set the
723 # volume from the volume control widgets.
724 def get_volume(self):
725 if util.platform == util.MAEMO:
726 return self.volume.get_level()/100.0
727 else:
728 return self.volume.get_value()
730 def set_volume(self, vol):
731 """ vol is a float from 0 to 1 """
732 assert 0 <= vol <= 1
733 if util.platform == util.MAEMO:
734 self.volume.set_level(vol*100.0)
735 else:
736 self.volume.set_value(vol)
738 def __set_volume_hide_timer(self, timeout, force_show=False):
739 if force_show or self.volume_button.get_active():
740 self.volume.show()
741 if self.volume_timer_id is not None:
742 gobject.source_remove(self.volume_timer_id)
744 self.volume_timer_id = gobject.timeout_add(
745 1000 * timeout, self.__volume_hide_callback )
747 def __volume_hide_callback(self):
748 self.volume_timer_id = None
749 self.volume.hide()
750 return False
752 def toggle_volumebar(self, widget=None):
753 if self.volume_timer_id is None:
754 self.__set_volume_hide_timer(5)
755 else:
756 self.__volume_hide_callback()
758 def volume_changed_gtk(self, widget, new_value=0.5):
759 player.volume_level = new_value
761 def volume_changed_hildon(self, widget):
762 self.__set_volume_hide_timer( 4, force_show=True )
763 player.volume_level = widget.get_level()/100.0
765 def mute_toggled(self, widget):
766 if widget.get_mute():
767 player.volume_level = 0
768 else:
769 player.volume_level = widget.get_level()/100.0
771 def show_main_window(self):
772 self.main_window.present()
774 def play_file(self, filename):
775 if self.check_queue():
776 self._play_file(filename)
778 def _play_file(self, filename, pause_on_load=False):
779 player.stop()
781 player.playlist.load( os.path.abspath(filename) )
782 if player.playlist.is_empty:
783 return False
785 player.play()
787 def on_player_stopped(self):
788 self.stop_progress_timer()
789 self.title_label.set_size_request(-1,-1)
790 self.reset_progress()
791 self.set_controls_sensitivity(False)
793 def on_player_playing(self):
794 self.start_progress_timer()
795 image(self.play_pause_button, 'media-playback-pause.png')
796 self.set_controls_sensitivity(True)
798 def on_player_new_track(self, metadata):
799 image(self.play_pause_button, 'media-playback-start.png')
800 self.play_pause_button.disconnect(self.button_handler_id)
801 self.button_handler_id = self.play_pause_button.connect(
802 'clicked', self.on_btn_play_pause_clicked )
804 for widget in [self.title_label,self.artist_label,self.album_label]:
805 widget.set_text('')
806 widget.hide()
808 self.cover_art.hide()
809 self.has_coverart = False
810 self.set_metadata(metadata)
812 text, position = player.get_formatted_position()
813 estimated_length = metadata.get('length', 0)
814 self.set_progress_callback( position, estimated_length )
816 def on_player_paused(self):
817 self.stop_progress_timer() # This should save some power
818 image(self.play_pause_button, 'media-playback-start.png')
820 def on_player_end_of_playlist(self):
821 self.play_pause_button.disconnect(self.button_handler_id)
822 self.button_handler_id = self.play_pause_button.connect(
823 'clicked', self.open_file_callback )
824 image(self.play_pause_button, gtk.STOCK_OPEN, True)
826 def on_file_queued(self, filepath, success, notify):
827 if notify:
828 filename = os.path.basename(filepath)
829 if success:
830 self.__log.info(
831 util.notify( '%s added successfully.' % filename ))
832 else:
833 self.__log.error(
834 util.notify( 'Error adding %s to the queue.' % filename))
836 def reset_progress(self):
837 self.progress.set_fraction(0)
838 self.set_progress_callback(0,0)
840 def set_progress_callback(self, time_elapsed, total_time):
841 """ times must be in nanoseconds """
842 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
843 util.convert_ns(total_time) )
844 self.progress.set_text( time_string )
845 fraction = float(time_elapsed) / float(total_time) if total_time else 0
846 self.progress.set_fraction( fraction )
848 def on_progressbar_changed(self, widget, event):
849 if ( not self.lock_progress and
850 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
851 new_fraction = event.x/float(widget.get_allocation().width)
852 resp = player.do_seek(percent=new_fraction)
853 if resp:
854 # Preemptively update the progressbar to make seeking smoother
855 self.set_progress_callback( *resp )
857 def on_btn_play_pause_clicked(self, widget=None):
858 player.play_pause_toggle()
860 def progress_timer_callback( self ):
861 if player.playing and not player.seeking:
862 pos_int, dur_int = player.get_position_duration()
863 # This prevents bogus values from being set while seeking
864 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
865 self.set_progress_callback( pos_int, dur_int )
866 return True
868 def start_progress_timer( self ):
869 if self.progress_timer_id is not None:
870 self.stop_progress_timer()
872 self.progress_timer_id = gobject.timeout_add(
873 1000, self.progress_timer_callback )
875 def stop_progress_timer( self ):
876 if self.progress_timer_id is not None:
877 gobject.source_remove( self.progress_timer_id )
878 self.progress_timer_id = None
880 def set_coverart( self, pixbuf ):
881 self.cover_art.set_from_pixbuf(pixbuf)
882 self.cover_art.show()
883 self.has_coverart = True
885 def set_metadata( self, tag_message ):
886 tags = { 'title': self.title_label, 'artist': self.artist_label,
887 'album': self.album_label }
889 if tag_message.has_key('image') and tag_message['image'] is not None:
890 value = tag_message['image']
892 pbl = gtk.gdk.PixbufLoader()
893 try:
894 pbl.write(value)
895 pbl.close()
896 pixbuf = pbl.get_pixbuf().scale_simple(
897 coverart_size[0], coverart_size[1], gtk.gdk.INTERP_BILINEAR )
898 self.set_coverart(pixbuf)
899 except Exception, e:
900 self.__log.exception('Error setting coverart...')
902 tag_vals = dict([ (i,'') for i in tags.keys()])
903 for tag,value in tag_message.iteritems():
904 if tags.has_key(tag) and value is not None and value.strip():
905 tags[tag].set_markup('<big>'+value+'</big>')
906 tag_vals[tag] = value
907 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
908 tags[tag].show()
909 if tag == 'title':
910 if util.platform == util.MAEMO:
911 self.main_window.set_title(value)
912 # oh man this is hacky :(
913 if self.has_coverart:
914 tags[tag].set_size_request(420,-1)
915 if len(value) >= 80: value = value[:80] + '...'
916 else:
917 self.main_window.set_title('Panucci - ' + value)
919 tags[tag].set_markup('<b><big>'+value+'</big></b>')
921 def seekbutton_callback( self, widget, seek_amount ):
922 resp = player.do_seek(from_current=seek_amount*10**9)
923 if resp:
924 # Preemptively update the progressbar to make seeking smoother
925 self.set_progress_callback( *resp )
927 def pickle_file_conversion(self):
928 pickle_file = os.path.expanduser('~/.rmp-bookmarks')
929 if os.path.isfile(pickle_file):
930 import pickle_converter
932 self.__log.info(
933 util.notify( _('Converting old pickle format to SQLite.') ))
934 self.__log.info( util.notify( _('This may take a while...') ))
936 if pickle_converter.load_pickle_file(pickle_file):
937 self.__log.info(
938 util.notify( _('Pickle file converted successfully.') ))
939 else:
940 self.__log.error( util.notify(
941 _('Error converting pickle file, check your log...') ))
943 def run(filename=None):
944 GTK_Main( filename )
945 gtk.main()
947 if __name__ == '__main__':
948 log.error( 'WARNING: Use the "panucci" executable to run this program.' )
949 log.error( 'Exiting...' )
950 sys.exit(1)