The mighty episode selector dialog and some menu re-arrangements
[gpodder.git] / src / gpodder / gui.py
blobcb16a06108040c1393a3ec5a483f2aef59b44a89
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
6 # gPodder 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 # gPodder 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 this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import webbrowser
29 from xml.sax import saxutils
31 from threading import Event
32 from threading import Thread
33 from string import strip
35 from gpodder import util
36 from gpodder import opml
37 from gpodder import services
38 from gpodder import download
39 from gpodder import SimpleGladeApp
41 from libpodcasts import podcastChannel
42 from libpodcasts import channelsToModel
43 from libpodcasts import load_channels
44 from libpodcasts import save_channels
46 from libgpodder import gPodderLib
47 from liblogger import log
49 from libplayers import UserAppsReader
51 from libipodsync import gPodder_iPodSync
52 from libipodsync import gPodder_FSSync
53 from libipodsync import ipod_supported
55 from libtagupdate import tagging_supported
57 app_name = "gpodder"
58 app_version = "unknown" # will be set in main() call
59 app_authors = [ 'Thomas Perl <thp@perli.net' ]
60 app_copyright = 'Copyright (c) 2005-2007 Thomas Perl'
61 app_website = 'http://gpodder.berlios.de/'
63 # these will be filled with pathnames in bin/gpodder
64 glade_dir = [ 'share', 'gpodder' ]
65 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
66 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
69 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
70 gpodder_main_window = None
72 def __init__( self, **kwargs):
73 path = os.path.join( glade_dir, '%s.glade' % app_name)
74 root = self.__class__.__name__
75 domain = app_name
77 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
79 if root == 'gPodder':
80 GladeWidget.gpodder_main_window = self.gPodder
81 else:
82 # If we have a child window, set it transient for our main window
83 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
85 if hasattr( self, 'center_on_widget'):
86 ( x, y ) = self.gpodder_main_window.get_position()
87 a = self.center_on_widget.allocation
88 ( x, y ) = ( x + a.x, y + a.y )
89 ( w, h ) = ( a.width, a.height )
90 ( pw, ph ) = getattr( self, root).get_size()
91 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
92 else:
93 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
95 def notification( self, message, title = None):
96 gobject.idle_add( self.show_message, message, title)
98 def show_message( self, message, title = None):
99 dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
101 if title:
102 dlg.set_title( title)
103 dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
104 else:
105 dlg.set_markup( '<span weight="bold" size="larger">%s</span>' % ( message ))
107 dlg.run()
108 dlg.destroy()
110 def show_confirmation( self, message, title = None):
111 dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
113 if title:
114 dlg.set_title( title)
115 dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
116 else:
117 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % message)
119 response = dlg.run()
120 dlg.destroy()
122 return response == gtk.RESPONSE_YES
124 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
125 if dst_filename == None:
126 dst_filename = src_filename
128 if dst_directory == None:
129 dst_directory = os.path.expanduser( '~')
131 ( base, extension ) = os.path.splitext( src_filename)
133 if not dst_filename.endswith( extension):
134 dst_filename += extension
136 dlg = gtk.FileChooserDialog( title = title, parent = GladeWidget.gpodder_main_window, action = gtk.FILE_CHOOSER_ACTION_SAVE)
137 dlg.set_do_overwrite_confirmation( True)
139 dlg.set_current_name( os.path.basename( dst_filename))
140 dlg.set_current_folder( dst_directory)
142 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
143 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
145 if dlg.run() == gtk.RESPONSE_OK:
146 dst_filename = dlg.get_filename()
147 if not dst_filename.endswith( extension):
148 dst_filename += extension
150 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
152 try:
153 shutil.copyfile( src_filename, dst_filename)
154 except:
155 log( 'Error copying file.', sender = self, traceback = True)
157 dlg.destroy()
161 class gPodder(GladeWidget):
162 def new(self):
163 self.uar = None
165 gl = gPodderLib()
167 gl.config.connect_gtk_window( self.gPodder)
168 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
170 while gtk.events_pending():
171 gtk.main_iteration( False)
173 if app_version.rfind( "svn") != -1:
174 self.gPodder.set_title( 'gPodder %s' % app_version)
176 self.default_title = self.gPodder.get_title()
178 # cell renderers for channel tree
179 namecolumn = gtk.TreeViewColumn( _('Channel'))
181 iconcell = gtk.CellRendererPixbuf()
182 namecolumn.pack_start( iconcell, False)
183 namecolumn.add_attribute( iconcell, 'pixbuf', 8)
185 namecell = gtk.CellRendererText()
186 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
187 namecolumn.pack_start( namecell, True)
188 namecolumn.add_attribute( namecell, 'markup', 7)
189 namecolumn.add_attribute( namecell, 'weight', 4)
191 newcell = gtk.CellRendererText()
192 namecolumn.pack_end( newcell, False)
193 namecolumn.add_attribute( newcell, 'text', 5)
194 namecolumn.add_attribute( newcell, 'weight', 4)
196 self.treeChannels.append_column( namecolumn)
198 # enable alternating colors hint
199 self.treeAvailable.set_rules_hint( True)
200 self.treeChannels.set_rules_hint( True)
202 # Add our context menu to treeAvailable
203 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
204 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
206 iconcell = gtk.CellRendererPixbuf()
207 iconcolumn = gtk.TreeViewColumn( _("Status"), iconcell, pixbuf = 4)
209 namecell = gtk.CellRendererText()
210 #namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
211 namecolumn = gtk.TreeViewColumn( _("Episode"), namecell, text = 1)
212 namecolumn.set_sizing( gtk.TREE_VIEW_COLUMN_AUTOSIZE)
214 sizecell = gtk.CellRendererText()
215 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
217 releasecell = gtk.CellRendererText()
218 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
220 desccell = gtk.CellRendererText()
221 desccell.set_property('ellipsize', pango.ELLIPSIZE_END)
222 desccolumn = gtk.TreeViewColumn( _("Description"), desccell, text=6)
224 for itemcolumn in ( iconcolumn, namecolumn, sizecolumn, releasecolumn, desccolumn ):
225 itemcolumn.set_resizable( True)
226 itemcolumn.set_reorderable( True)
227 self.treeAvailable.append_column( itemcolumn)
229 # enable search in treeavailable
230 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
232 # enable multiple selection support
233 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
234 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
236 # columns and renderers for "download progress" tab
237 episodecell = gtk.CellRendererText()
238 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
240 speedcell = gtk.CellRendererText()
241 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
243 progresscell = gtk.CellRendererProgress()
244 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
246 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
247 self.treeDownloads.append_column( itemcolumn)
249 services.download_status_manager.register( 'list-changed', self.download_status_updated)
250 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
252 self.treeDownloads.set_model( services.download_status_manager.tree_model)
254 #Add Drag and Drop Support
255 flags = gtk.DEST_DEFAULT_ALL
256 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
257 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
258 self.treeChannels.drag_dest_set( flags, targets, actions)
259 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
261 # Subscribed channels
262 self.active_channel = None
263 self.channels = load_channels( load_items = False, offline = True)
265 # load list of user applications
266 self.user_apps_reader = UserAppsReader()
267 Thread( target = self.user_apps_reader.read).start()
269 # Clean up old, orphaned download files
270 gl.clean_up_downloads( delete_partial = True)
272 # Set the "Device" menu item for the first time
273 self.update_item_device()
275 # Now, update the feed cache, when everything's in place
276 self.update_feed_cache( force_update = gl.config.update_on_startup)
278 def treeview_channels_button_pressed( self, treeview, event):
279 if event.button == 3:
280 ( x, y ) = ( int(event.x), int(event.y) )
281 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
283 paths = []
285 # Did the user right-click into a selection?
286 selection = treeview.get_selection()
287 if selection.count_selected_rows() and path:
288 ( model, paths ) = selection.get_selected_rows()
289 if path not in paths:
290 # We have right-clicked, but not into the
291 # selection, assume we don't want to operate
292 # on the selection
293 paths = []
295 # No selection or right click not in selection:
296 # Select the single item where we clicked
297 if not len( paths) and path:
298 treeview.grab_focus()
299 treeview.set_cursor( path, column, 0)
301 ( model, paths ) = ( treeview.get_model(), [ path ] )
303 # We did not find a selection, and the user didn't
304 # click on an item to select -- don't show the menu
305 if not len( paths):
306 return True
308 menu = gtk.Menu()
310 channel_title = model.get_value( model.get_iter( paths[0]), 1)
312 item = gtk.ImageMenuItem( _('Open download folder'))
313 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
314 item.connect( 'activate', lambda x: gPodderLib().open_folder( self.active_channel.save_dir))
315 menu.append( item)
317 menu.append( gtk.SeparatorMenuItem())
319 item = gtk.ImageMenuItem('')
320 ( label, image ) = item.get_children()
321 label.set_text( _('Edit %s') % channel_title)
322 item.set_image( gtk.image_new_from_stock( gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU))
323 item.connect( 'activate', self.on_itemEditChannel_activate)
324 menu.append( item)
326 item = gtk.ImageMenuItem( _('Remove %s') % ( channel_title, ))
327 item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
328 item.connect( 'activate', self.on_itemRemoveChannel_activate)
329 menu.append( item)
331 menu.show_all()
332 menu.popup( None, None, None, event.button, event.time)
334 return True
336 def save_episode_as_file( self, url, *args):
337 episode = self.active_channel.find_episode( url)
339 self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
341 def treeview_button_pressed( self, treeview, event):
342 if event.button == 3:
343 ( x, y ) = ( int(event.x), int(event.y) )
344 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
346 paths = []
348 # Did the user right-click into a selection?
349 selection = self.treeAvailable.get_selection()
350 if selection.count_selected_rows() and path:
351 ( model, paths ) = selection.get_selected_rows()
352 if path not in paths:
353 # We have right-clicked, but not into the
354 # selection, assume we don't want to operate
355 # on the selection
356 paths = []
358 # No selection or right click not in selection:
359 # Select the single item where we clicked
360 if not len( paths) and path:
361 treeview.grab_focus()
362 treeview.set_cursor( path, column, 0)
364 ( model, paths ) = ( treeview.get_model(), [ path ] )
366 # We did not find a selection, and the user didn't
367 # click on an item to select -- don't show the menu
368 if not len( paths):
369 return True
371 first_url = model.get_value( model.get_iter( paths[0]), 0)
373 menu = gtk.Menu()
375 ( can_play, can_download, can_transfer, can_cancel ) = self.play_or_download()
377 if len(paths) == 1:
378 # Single item, add episode information menu item
379 episode_title = model.get_value( model.get_iter( paths[0]), 1)
380 episode_url = model.get_value( model.get_iter( paths[0]), 0)
381 if len(episode_title) > 30:
382 episode_title = episode_title[:27] + '...'
383 item = gtk.ImageMenuItem('')
384 ( label, image ) = item.get_children()
385 label.set_text( _('Episode information: %s') % episode_title)
386 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
387 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
388 menu.append( item)
389 if can_play:
390 item = gtk.ImageMenuItem( _('Save %s to folder...') % episode_title)
391 item.set_image( gtk.image_new_from_stock( gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
392 item.connect( 'activate', lambda w: self.save_episode_as_file( episode_url))
393 menu.append( item)
394 menu.append( gtk.SeparatorMenuItem())
395 else:
396 episode_title = _('%d selected episodes') % len(paths)
398 if can_play:
399 item = gtk.ImageMenuItem( _('Play %s') % episode_title)
400 item.set_image( gtk.image_new_from_stock( gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
401 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
402 menu.append( item)
403 item = gtk.ImageMenuItem( _('Remove %s') % episode_title)
404 item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
405 item.connect( 'activate', self.on_btnDownloadedDelete_clicked)
406 menu.append( item)
408 if can_download:
409 item = gtk.ImageMenuItem( _('Download %s') % episode_title)
410 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
411 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
412 menu.append( item)
414 menu.append( gtk.SeparatorMenuItem())
415 is_downloaded = gPodderLib().history_is_downloaded( first_url)
416 if is_downloaded:
417 item = gtk.ImageMenuItem( _('Mark %s as not downloaded') % episode_title)
418 item.set_image( gtk.image_new_from_stock( gtk.STOCK_UNDELETE, gtk.ICON_SIZE_MENU))
419 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, False))
420 menu.append( item)
421 else:
422 item = gtk.ImageMenuItem( _('Mark %s as deleted') % episode_title)
423 item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
424 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, True))
425 menu.append( item)
427 if can_transfer:
428 item = gtk.ImageMenuItem( _('Transfer %s to %s') % ( episode_title, gPodderLib().get_device_name() ))
429 item.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU))
430 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolTransfer))
431 menu.append( item)
433 if can_play:
434 menu.append( gtk.SeparatorMenuItem())
435 is_played = gPodderLib().history_is_played( first_url)
436 if is_played:
437 item = gtk.ImageMenuItem( _('Mark %s as unplayed') % episode_title)
438 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
439 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
440 menu.append( item)
441 else:
442 item = gtk.ImageMenuItem( _('Mark %s as played') % episode_title)
443 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
444 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
445 menu.append( item)
447 if can_cancel:
448 item = gtk.ImageMenuItem( _('_Cancel download'))
449 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
450 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
451 menu.append( item)
453 menu.show_all()
454 menu.popup( None, None, None, event.button, event.time)
456 return True
458 def download_progress_updated( self, count, percentage):
459 title = [ self.default_title ]
461 if count == 1:
462 title.append( _('downloading one file'))
463 elif count > 1:
464 title.append( _('downloading %d files') % count)
466 if len(title) == 2:
467 title[1] = ''.join( [ title[1], ' (%d%%)' % ( percentage, ) ])
469 self.gPodder.set_title( ' - '.join( title))
471 def playback_episode( self, current_channel, current_podcast):
472 ( success, application ) = gPodderLib().playback_episode( current_channel, current_podcast)
473 if not success:
474 self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), ))
475 self.download_status_updated()
477 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
478 if model == None:
479 return True
481 key = key.lower()
483 # columns, as defined in libpodcasts' get model method
484 # 1 = episode title, 7 = description
485 columns = (1, 7)
487 for column in columns:
488 value = model.get_value( iter, column).lower()
489 if value.find( key) != -1:
490 return False
492 return True
494 def play_or_download( self):
495 if self.wNotebook.get_current_page() > 0:
496 return
498 ( can_play, can_download, can_transfer, can_cancel ) = (False,)*4
500 selection = self.treeAvailable.get_selection()
501 if selection.count_selected_rows() > 0:
502 (model, paths) = selection.get_selected_rows()
504 for path in paths:
505 url = model.get_value( model.get_iter( path), 0)
506 local_filename = model.get_value( model.get_iter( path), 8)
508 if os.path.exists( local_filename):
509 can_play = True
510 else:
511 if services.download_status_manager.is_download_in_progress( url):
512 can_cancel = True
513 else:
514 can_download = True
516 if util.file_type_by_extension( util.file_extension_from_url( url)) == 'torrent':
517 can_download = can_download or gPodderLib().config.use_gnome_bittorrent
519 can_download = can_download and not can_cancel
520 can_play = can_play and not can_cancel and not can_download
521 can_transfer = can_play and gPodderLib().config.device_type != 'none'
523 self.toolPlay.set_sensitive( can_play)
524 self.toolDownload.set_sensitive( can_download)
525 self.toolTransfer.set_sensitive( can_transfer)
526 self.toolCancel.set_sensitive( can_cancel)
528 return ( can_play, can_download, can_transfer, can_cancel )
530 def download_status_updated( self):
531 count = services.download_status_manager.count()
532 if count:
533 self.labelDownloads.set_text( _('Downloads (%d)') % count)
534 else:
535 self.labelDownloads.set_text( _('Downloads'))
537 for channel in self.channels:
538 channel.update_model()
540 self.updateComboBox()
542 def updateComboBox( self):
543 ( model, iter ) = self.treeChannels.get_selection().get_selected()
545 if model and iter:
546 selected = model.get_path( iter)
547 else:
548 selected = (0,)
550 rect = self.treeChannels.get_visible_rect()
551 self.treeChannels.set_model( channelsToModel( self.channels))
552 self.treeChannels.scroll_to_point( rect.x, rect.y)
553 while gtk.events_pending():
554 gtk.main_iteration( False)
555 self.treeChannels.scroll_to_point( rect.x, rect.y)
557 try:
558 self.treeChannels.get_selection().select_path( selected)
559 except:
560 log( 'Cannot set selection on treeChannels', sender = self)
561 self.on_treeChannels_cursor_changed( self.treeChannels)
563 def updateTreeView( self):
564 gl = gPodderLib()
566 if self.channels:
567 self.treeAvailable.set_model( self.active_channel.tree_model)
568 self.treeAvailable.columns_autosize()
569 self.play_or_download()
570 else:
571 if self.treeAvailable.get_model():
572 self.treeAvailable.get_model().clear()
574 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
575 result = sel.data
576 self.add_new_channel( result)
578 def add_new_channel( self, result = None, ask_download_new = True):
579 result = util.normalize_feed_url( result)
581 if result:
582 for old_channel in self.channels:
583 if old_channel.url == result:
584 self.show_message( _('You have already subscribed to this channel: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
585 log( 'Channel already exists: %s', result)
586 # Select the existing channel in combo box
587 for i in range( len( self.channels)):
588 if self.channels[i] == old_channel:
589 self.treeChannels.get_selection().select_path( (i,))
590 return
591 log( 'Adding new channel: %s', result)
592 try:
593 channel = podcastChannel.get_by_url( url = result, force_update = True)
594 except:
595 channel = None
597 if channel:
598 self.channels.append( channel)
599 save_channels( self.channels)
600 # download changed channels
601 self.update_feed_cache( force_update = False)
603 (username, password) = util.username_password_from_url( result)
604 if username and self.show_confirmation( _('You have supplied <b>%s</b> as username and a password for this feed. Would you like to use the same authentication data for downloading episodes?') % ( saxutils.escape( username), ), _('Password authentication')):
605 channel.username = username
606 channel.password = password
607 log('Saving authentication data for episode downloads..', sender = self)
608 channel.save_settings()
610 # ask user to download some new episodes
611 self.treeChannels.get_selection().select_path( (len( self.channels)-1,))
612 self.active_channel = channel
613 self.updateTreeView()
614 if ask_download_new:
615 self.on_btnDownloadNewer_clicked( None)
616 else:
617 title = _('Error adding channel')
618 message = _('The channel could not be added. Please check the spelling of the URL or try again later.')
619 self.show_message( message, title)
620 else:
621 if result:
622 title = _('URL scheme not supported')
623 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
624 self.show_message( message, title)
626 def sync_to_ipod_proc( self, sync, sync_win, episodes = None):
627 if not sync.open():
628 sync.close( success = False, access_error = True)
629 return False
631 if episodes == None:
632 i = 0
633 available_channels = [ c.load_downloaded_episodes() for c in self.channels ]
634 downloaded_channels = [ c for c in available_channels if len(c) ]
635 for channel in downloaded_channels:
636 sync.set_progress_overall( i, len(downloaded_channels))
637 channel.load_settings()
638 sync.sync_channel( channel, sync_played_episodes = not gPodderLib().config.only_sync_not_played)
639 i += 1
640 sync.set_progress_overall( i, len(downloaded_channels))
641 else:
642 sync_win.pbSync.hide_all()
643 sync.sync_channel( self.active_channel, episodes, True)
645 sync.close( success = not sync.cancelled)
646 # update model for played state updates after sync
647 for channel in self.channels:
648 gobject.idle_add( channel.update_model)
649 gobject.idle_add( self.updateTreeView)
651 def ipod_cleanup_proc( self, sync):
652 if not sync.open():
653 gobject.idle_add( self.show_message, message, title)
654 sync.close( success = False, access_error = True)
655 return False
657 sync.clean_playlist()
658 sync.close( success = not sync.cancelled, cleaned = True)
659 gobject.idle_add( self.updateTreeView)
661 def update_feed_cache_callback( self, label, progressbar, position, count):
662 if len(self.channels) > position:
663 title = _('Updating %s') % saxutils.escape( self.channels[position].title)
664 else:
665 title = _('Please wait...')
667 label.set_markup( '<i>%s</i>' % title)
669 progressbar.set_text( _('%d of %d channels updated') % ( position, count ))
670 if count:
671 progressbar.set_fraction( ((1.00*position) / (1.00*count)))
673 def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
674 self.channels = load_channels( force_update = force_update, callback_proc = callback_proc, callback_error = callback_error, offline = not force_update)
675 if finish_proc:
676 finish_proc()
678 def update_feed_cache(self, force_update = True):
679 title = _('Downloading podcast feeds')
680 heading = _('Downloading feeds')
681 body = _('Podcast feeds contain channel metadata and information about current episodes.')
683 please_wait = gtk.Dialog( title, self.gPodder, gtk.DIALOG_MODAL, ( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, ))
684 please_wait.set_transient_for( self.gPodder)
685 please_wait.set_position( gtk.WIN_POS_CENTER_ON_PARENT)
686 please_wait.vbox.set_spacing( 5)
687 please_wait.set_border_width( 10)
688 please_wait.set_resizable( False)
690 label_heading = gtk.Label()
691 label_heading.set_alignment( 0.0, 0.5)
692 label_heading.set_markup( '<span weight="bold" size="larger">%s</span>' % heading)
694 label_body = gtk.Label()
695 label_body.set_text( body)
696 label_body.set_alignment( 0.0, 0.5)
697 label_body.set_line_wrap( True)
699 myprogressbar = gtk.ProgressBar()
701 mylabel = gtk.Label()
702 mylabel.set_alignment( 0.0, 0.5)
703 mylabel.set_ellipsize( pango.ELLIPSIZE_END)
705 # put it all together
706 please_wait.vbox.pack_start( label_heading)
707 please_wait.vbox.pack_start( label_body)
708 please_wait.vbox.pack_start( myprogressbar)
709 please_wait.vbox.pack_end( mylabel)
710 please_wait.show_all()
712 # center the dialog on the gPodder main window
713 ( x, y ) = self.gPodder.get_position()
714 ( w, h ) = self.gPodder.get_size()
715 ( pw, ph ) = please_wait.get_size()
716 please_wait.move( x + w/2 - pw/2, y + h/2 - ph/2)
718 # hide separator line
719 please_wait.set_has_separator( False)
721 # let's get down to business..
722 callback_proc = lambda pos, count: gobject.idle_add( self.update_feed_cache_callback, mylabel, myprogressbar, pos, count)
723 callback_error = lambda x: gobject.idle_add( self.show_message, x)
724 finish_proc = lambda: gobject.idle_add( please_wait.destroy)
726 args = ( force_update, callback_proc, callback_error, finish_proc, )
728 thread = Thread( target = self.update_feed_cache_proc, args = args)
729 thread.start()
731 please_wait.run()
733 self.updateComboBox()
735 # download all new?
736 if force_update and gPodderLib().config.download_after_update:
737 self.on_itemDownloadAllNew_activate( self.gPodder)
739 def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
740 if self.active_channel == None:
741 return
743 current_channel = self.active_channel
744 current_podcast = current_channel.find_episode( url)
745 filename = current_podcast.local_filename()
747 if widget:
748 if (widget.get_name() == 'itemPlaySelected' or widget.get_name() == 'toolPlay') and os.path.exists( filename):
749 # addDownloadedItem just to make sure the episode is marked correctly in localdb
750 current_channel.addDownloadedItem( current_podcast)
751 # open the file now
752 if current_podcast.file_type() != 'torrent':
753 self.playback_episode( current_channel, current_podcast)
754 return
756 if widget.get_name() == 'treeAvailable':
757 play_callback = lambda: self.playback_episode( current_channel, current_podcast)
758 download_callback = lambda: self.download_podcast_by_url( url, want_message_dialog, None)
759 gpe = gPodderEpisode( episode = current_podcast, channel = current_channel, download_callback = download_callback, play_callback = play_callback, center_on_widget = self.treeAvailable)
760 return
762 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( current_podcast.url):
763 download.DownloadThread( current_channel, current_podcast, self.notification).start()
764 else:
765 if want_message_dialog and os.path.exists( filename) and not current_podcast.file_type() == 'torrent':
766 title = _('Episode already downloaded')
767 message = _('You have already downloaded this episode. Click on the episode to play it.')
768 self.show_message( message, title)
769 elif want_message_dialog and not current_podcast.file_type() == 'torrent':
770 title = _('Download in progress')
771 message = _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
772 self.show_message( message, title)
774 if os.path.exists( filename):
775 log( 'Episode has already been downloaded.')
776 current_channel.addDownloadedItem( current_podcast)
777 self.updateComboBox()
779 def close_gpodder(self, widget, *args):
780 if self.channels:
781 save_channels( self.channels)
783 services.download_status_manager.cancel_all()
785 gl = gPodderLib()
787 self.gtk_main_quit()
788 sys.exit( 0)
790 def for_each_selected_episode_url( self, callback):
791 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
792 for path in paths:
793 url = model.get_value( model.get_iter( path), 0)
794 try:
795 callback( url)
796 except:
797 log( 'Warning: Error in for_each_selected_episode_url for URL %s', url, sender = self)
798 self.active_channel.update_model()
799 self.updateComboBox()
801 def delete_episode_list( self, episodes, confirm = True):
802 if len(episodes) == 0:
803 return
805 if len(episodes) == 1:
806 message = _('Do you really want to delete this episode?')
807 else:
808 message = _('Do you really want to delete %d episodes?') % len(episodes)
810 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
811 return
813 for episode in episodes:
814 log('Deleting episode: %s', episode.title, sender = self)
815 episode.delete_from_disk()
817 self.download_status_updated()
819 def on_itemRemoveOldEpisodes_activate( self, widget):
820 columns = (
821 ('title', _('Episode')),
822 ('channel_prop', _('Channel')),
823 ('filesize_prop', _('Size')),
824 ('pubdate_prop', _('Released')),
825 ('played_prop', _('Status')),
828 selection_buttons = {
829 _('Select played'): lambda episode: episode.channel.is_played( episode),
832 instructions = _('Select the episodes you want to delete from your hard disk.')
834 episodes = []
835 selected = []
836 for channel in self.channels:
837 for episode in channel:
838 if episode.is_downloaded():
839 episodes.append( episode)
840 selected.append( channel.is_played( episode))
842 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
843 episodes = episodes, selected = selected, columns = columns, \
844 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
845 selection_buttons = selection_buttons)
847 def on_item_toggle_downloaded_activate( self, widget, toggle = True, new_value = False):
848 if toggle:
849 callback = lambda url: gPodderLib().history_mark_downloaded( url, not gPodderLib().history_is_downloaded( url))
850 else:
851 callback = lambda url: gPodderLib().history_mark_downloaded( url, new_value)
853 self.for_each_selected_episode_url( callback)
855 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
856 if toggle:
857 callback = lambda url: gPodderLib().history_mark_played( url, not gPodderLib().history_is_played( url))
858 else:
859 callback = lambda url: gPodderLib().history_mark_played( url, new_value)
861 self.for_each_selected_episode_url( callback)
863 def on_itemUpdate_activate(self, widget, *args):
864 if self.channels:
865 self.update_feed_cache()
866 else:
867 title = _('No channels available')
868 message = _('You need to subscribe to some podcast feeds before you can start downloading podcasts. Use your favorite search engine to look for interesting podcasts.')
869 self.show_message( message, title)
871 def download_episode_list( self, episodes):
872 for episode in episodes:
873 log('Downloading episode: %s', episode.title, sender = self)
874 filename = episode.local_filename()
875 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
876 download.DownloadThread( episode.channel, episode, self.notification).start()
878 def on_itemDownloadAllNew_activate(self, widget, *args):
879 columns = (
880 ('title', _('Episode')),
881 ('channel_prop', _('Channel')),
882 ('filesize_prop', _('Size')),
883 ('pubdate_prop', _('Released')),
886 episodes = []
888 for channel in self.channels:
889 for episode in channel.get_new_episodes():
890 episodes.append( episode)
892 if len(episodes) > 0:
893 instructions = _('Select the episodes you want to download now.')
895 gPodderEpisodeSelector( title = _('New episodes available'), instructions = instructions, \
896 episodes = episodes, columns = columns, selected_default = True, \
897 callback = self.download_episode_list)
898 else:
899 title = _('No new episodes')
900 message = _('There are no new episodes to download from your podcast subscriptions. Please check for new episodes later.')
901 self.show_message( message, title)
903 def on_sync_to_ipod_activate(self, widget, *args):
904 gl = gPodderLib()
905 if gl.config.device_type == 'none':
906 title = _('No device configured')
907 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
908 self.show_message( message, title)
909 return
911 if gl.config.device_type == 'ipod' and not ipod_supported():
912 title = _('Libraries needed: gpod, pymad')
913 message = _('To use the iPod synchronization feature, you need to install the <b>python-gpod</b> and <b>python-pymad</b> libraries from your distribution vendor. More information about the needed libraries can be found on the gPodder website.')
914 self.show_message( message, title)
915 return
917 if gl.config.device_type in [ 'ipod', 'filesystem' ]:
918 sync_class = None
920 if gl.config.device_type == 'filesystem':
921 sync_class = gPodder_FSSync
922 elif gl.config.device_type == 'ipod':
923 sync_class = gPodder_iPodSync
925 if not sync_class:
926 return
928 sync_win = gPodderSync()
929 sync = sync_class( callback_status = sync_win.set_status, callback_progress = sync_win.set_progress, callback_done = sync_win.close)
930 sync_win.set_sync_object( sync)
931 thread_args = [ sync, sync_win ]
932 if widget == None:
933 thread_args.append( args[0])
934 thread = Thread( target = self.sync_to_ipod_proc, args = thread_args)
935 thread.start()
937 def on_cleanup_ipod_activate(self, widget, *args):
938 gl = gPodderLib()
939 if gl.config.device_type == 'none':
940 title = _('No device configured')
941 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
942 self.show_message( message, title)
943 return
945 if gl.config.device_type == 'ipod' and not ipod_supported():
946 title = _('Libraries needed: gpod, pymad')
947 message = _('To use the iPod synchronization feature, you need to install the <b>python-gpod</b> and <b>python-pymad</b> libraries from your distribution vendor. More information about the needed libraries can be found on the gPodder website.')
948 self.show_message( message, title)
949 return
951 if gl.config.device_type in [ 'ipod', 'filesystem' ]:
952 sync_class = None
954 if gl.config.device_type == 'filesystem':
955 title = _('Delete podcasts from MP3 player?')
956 message = _('Do you really want to completely remove all episodes from your MP3 player?')
957 if self.show_confirmation( message, title):
958 sync_class = gPodder_FSSync
959 elif gl.config.device_type == 'ipod':
960 title = _('Delete podcasts on iPod?')
961 message = _('Do you really want to completely remove all episodes in the <b>Podcasts</b> playlist on your iPod?')
962 if self.show_confirmation( message, title):
963 sync_class = gPodder_iPodSync
965 if not sync_class:
966 return
968 sync_win = gPodderSync()
969 sync = sync_class( callback_status = sync_win.set_status, callback_progress = sync_win.set_progress, callback_done = sync_win.close)
970 sync_win.set_sync_object( sync)
971 thread = Thread( target = self.ipod_cleanup_proc, args = ( sync, ))
972 thread.start()
974 def update_item_device( self):
975 gl = gPodderLib()
977 if gl.config.device_type != 'none':
978 self.itemDevice.show_all()
979 ( label, image ) = self.itemDevice.get_children()
980 label.set_text( gl.get_device_name())
981 else:
982 self.itemDevice.hide_all()
984 def properties_closed( self):
985 self.update_item_device()
986 self.updateComboBox()
988 def on_itemPreferences_activate(self, widget, *args):
989 prop = gPodderProperties( callback_finished = self.properties_closed)
990 prop.set_uar( self.user_apps_reader)
992 def on_itemAddChannel_activate(self, widget, *args):
993 if self.channelPaned.get_position() < 200:
994 self.channelPaned.set_position( 200)
995 self.entryAddChannel.set_text( _('Enter podcast URL'))
996 self.entryAddChannel.grab_focus()
998 def on_itemEditChannel_activate(self, widget, *args):
999 if self.active_channel == None:
1000 title = _('No channel selected')
1001 message = _('Please select a channel in the channels list to edit.')
1002 self.show_message( message, title)
1003 return
1005 gPodderChannel( channel = self.active_channel, callback_closed = self.updateComboBox)
1007 def on_itemRemoveChannel_activate(self, widget, *args):
1008 try:
1009 title = _('Remove channel and episodes?')
1010 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % ( self.active_channel.title, )
1011 if self.show_confirmation( message, title):
1012 self.active_channel.remove_downloaded()
1013 # only delete partial files if we do not have any downloads in progress
1014 delete_partial = not services.download_status_manager.has_items()
1015 gPodderLib().clean_up_downloads( delete_partial)
1016 self.channels.remove( self.active_channel)
1017 save_channels( self.channels)
1018 if len(self.channels) > 0:
1019 self.treeChannels.get_selection().select_path( (len( self.channels)-1,))
1020 self.active_channel = self.channels[len( self.channels)-1]
1021 self.update_feed_cache( force_update = False)
1022 except:
1023 pass
1025 def on_itemExportChannels_activate(self, widget, *args):
1026 if not self.channels:
1027 title = _('Nothing to export')
1028 message = _('Your list of channel subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1029 self.show_message( message, title)
1030 return
1032 dlg = gtk.FileChooserDialog( title=_("Export to OPML"), parent = None, action = gtk.FILE_CHOOSER_ACTION_SAVE)
1033 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1034 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
1035 response = dlg.run()
1036 if response == gtk.RESPONSE_OK:
1037 filename = dlg.get_filename()
1038 exporter = opml.Exporter( filename)
1039 if not exporter.write( self.channels):
1040 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1042 dlg.destroy()
1044 def on_itemImportChannels_activate(self, widget, *args):
1045 gPodderOpmlLister().get_channels_from_url( gPodderLib().config.opml_url, lambda url: self.add_new_channel(url,False), lambda: self.on_itemDownloadAllNew_activate( self.gPodder))
1047 def on_btnTransfer_clicked(self, widget, *args):
1048 self.on_treeAvailable_row_activated( widget, args)
1050 def on_homepage_activate(self, widget, *args):
1051 Thread( target = webbrowser.open, args = ( app_website, )).start()
1053 def on_wishlist_activate(self, widget, *args):
1054 Thread( target = webbrowser.open, args = ( 'http://www.amazon.de/gp/registry/2PD2MYGHE6857', )).start()
1056 def on_mailinglist_activate(self, widget, *args):
1057 Thread( target = webbrowser.open, args = ( 'http://lists.berlios.de/mailman/listinfo/gpodder-devel', )).start()
1059 def on_itemAbout_activate(self, widget, *args):
1060 dlg = gtk.AboutDialog()
1061 dlg.set_name( app_name)
1062 dlg.set_version( app_version)
1063 dlg.set_authors( app_authors)
1064 dlg.set_copyright( app_copyright)
1065 dlg.set_website( app_website)
1066 dlg.set_translator_credits( _('translator-credits'))
1067 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
1069 try:
1070 dlg.set_logo( gtk.gdk.pixbuf_new_from_file_at_size( scalable_dir, 200, 200))
1071 except:
1072 pass
1074 dlg.run()
1076 def on_wNotebook_switch_page(self, widget, *args):
1077 page_num = args[1]
1078 if page_num == 0:
1079 self.play_or_download()
1080 else:
1081 self.toolDownload.set_sensitive( False)
1082 self.toolPlay.set_sensitive( False)
1083 self.toolTransfer.set_sensitive( False)
1084 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
1086 def on_treeChannels_row_activated(self, widget, *args):
1087 self.on_itemEditChannel_activate( self.treeChannels)
1089 def on_treeChannels_cursor_changed(self, widget, *args):
1090 ( model, iter ) = self.treeChannels.get_selection().get_selected()
1092 if model != None and iter != None:
1093 id = model.get_path( iter)[0]
1094 self.active_channel = self.channels[id]
1096 self.itemEditChannel.get_child().set_text( _('Edit "%s"') % ( self.active_channel.title,))
1097 self.itemRemoveChannel.get_child().set_text( _('Remove "%s"') % ( self.active_channel.title,))
1098 self.itemEditChannel.show_all()
1099 self.itemRemoveChannel.show_all()
1100 else:
1101 self.active_channel = None
1102 self.itemEditChannel.hide_all()
1103 self.itemRemoveChannel.hide_all()
1105 self.updateTreeView()
1107 def on_entryAddChannel_changed(self, widget, *args):
1108 active = self.entryAddChannel.get_text() not in ('', _('Enter podcast URL'))
1109 self.btnAddChannel.set_sensitive( active)
1111 def on_btnAddChannel_clicked(self, widget, *args):
1112 url = self.entryAddChannel.get_text()
1113 self.entryAddChannel.set_text('')
1114 self.add_new_channel( url)
1116 def on_btnEditChannel_clicked(self, widget, *args):
1117 self.on_itemEditChannel_activate( widget, args)
1119 def on_treeAvailable_row_activated(self, widget, *args):
1120 try:
1121 selection = self.treeAvailable.get_selection()
1122 selection_tuple = selection.get_selected_rows()
1123 transfer_files = False
1124 episodes = []
1126 if selection.count_selected_rows() > 1:
1127 widget_to_send = None
1128 show_message_dialog = False
1129 else:
1130 widget_to_send = widget
1131 show_message_dialog = True
1133 if widget.get_name() == 'itemTransferSelected' or widget.get_name() == 'toolTransfer':
1134 transfer_files = True
1136 for apath in selection_tuple[1]:
1137 selection_iter = self.treeAvailable.get_model().get_iter( apath)
1138 url = self.treeAvailable.get_model().get_value( selection_iter, 0)
1140 if transfer_files:
1141 episodes.append( self.active_channel.find_episode( url))
1142 else:
1143 self.download_podcast_by_url( url, show_message_dialog, widget_to_send)
1145 if transfer_files and len(episodes):
1146 self.on_sync_to_ipod_activate( None, episodes)
1147 except:
1148 title = _('Nothing selected')
1149 message = _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
1150 self.show_message( message, title)
1152 def on_btnDownload_clicked(self, widget, *args):
1153 self.on_treeAvailable_row_activated( widget, args)
1155 def on_treeAvailable_button_release_event(self, widget, *args):
1156 self.play_or_download()
1158 def on_btnDownloadNewer_clicked(self, widget, *args):
1159 channel = self.active_channel
1160 episodes_to_download = channel.get_new_episodes()
1162 if not episodes_to_download:
1163 title = _('No episodes to download')
1164 message = _('You have already downloaded the most recent episodes from <b>%s</b>.') % ( channel.title, )
1165 self.show_message( message, title)
1166 else:
1167 if len(episodes_to_download) > 1:
1168 if len(episodes_to_download) < 10:
1169 e_str = '\n'.join( [ ' <b>'+saxutils.escape(e.title)+'</b>' for e in episodes_to_download ] )
1170 else:
1171 e_str = '\n'.join( [ ' <b>'+saxutils.escape(e.title)+'</b>' for e in episodes_to_download[:7] ] )
1172 e_str_2 = _('(...%d more episodes...)') % ( len(episodes_to_download)-7, )
1173 e_str = '%s\n <i>%s</i>' % ( e_str, e_str_2, )
1174 title = _('Download new episodes?')
1175 message = _('New episodes are available for download. If you want, you can download these episodes to your computer now.')
1176 message = '%s\n\n%s' % ( message, e_str, )
1177 else:
1178 title = _('Download %s?') % saxutils.escape(episodes_to_download[0].title)
1179 message = _('A new episode is available for download. If you want, you can download this episode to your computer now.')
1181 if not self.show_confirmation( message, title):
1182 return
1184 for episode in episodes_to_download:
1185 self.download_podcast_by_url( episode.url, False)
1187 def on_btnSelectAllAvailable_clicked(self, widget, *args):
1188 self.treeAvailable.get_selection().select_all()
1189 self.on_treeAvailable_row_activated( self.toolDownload, args)
1190 self.treeAvailable.get_selection().unselect_all()
1192 def on_treeDownloads_row_activated(self, widget, *args):
1193 cancel_urls = []
1195 if self.wNotebook.get_current_page() > 0:
1196 # Use the download list treeview + model
1197 ( tree, column ) = ( self.treeDownloads, 3 )
1198 else:
1199 # Use the available podcasts treeview + model
1200 ( tree, column ) = ( self.treeAvailable, 0 )
1202 selection = tree.get_selection()
1203 (model, paths) = selection.get_selected_rows()
1204 for path in paths:
1205 url = model.get_value( model.get_iter( path), column)
1206 cancel_urls.append( url)
1208 if len( cancel_urls) == 0:
1209 log('Nothing selected.', sender = self)
1210 return
1212 if len( cancel_urls) == 1:
1213 title = _('Cancel download?')
1214 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
1215 else:
1216 title = _('Cancel downloads?')
1217 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
1219 if self.show_confirmation( message, title):
1220 for url in cancel_urls:
1221 services.download_status_manager.cancel_by_url( url)
1223 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
1224 self.on_treeDownloads_row_activated( widget, None)
1226 def on_btnCancelAll_clicked(self, widget, *args):
1227 self.treeDownloads.get_selection().select_all()
1228 self.on_treeDownloads_row_activated( self.toolCancel, None)
1229 self.treeDownloads.get_selection().unselect_all()
1231 def on_btnDownloadedExecute_clicked(self, widget, *args):
1232 self.on_treeAvailable_row_activated( widget, args)
1234 def on_btnDownloadedDelete_clicked(self, widget, *args):
1235 if self.active_channel == None:
1236 return
1238 channel_url = self.active_channel.url
1239 selection = self.treeAvailable.get_selection()
1240 ( model, paths ) = selection.get_selected_rows()
1242 if selection.count_selected_rows() == 0:
1243 log( 'Nothing selected - will not remove any downloaded episode.')
1244 return
1246 if selection.count_selected_rows() == 1:
1247 title = _('Remove %s?') % model.get_value( model.get_iter( paths[0]), 1)
1248 message = _("If you remove this episode, it will be deleted from your computer. If you want to listen to this episode again, you will have to re-download it.")
1249 else:
1250 title = _('Remove %d episodes?') % selection.count_selected_rows()
1251 message = _('If you remove these episodes, they will be deleted from your computer. If you want to listen to any of these episodes again, you will have to re-download the episodes in question.')
1253 # if user confirms deletion, let's remove some stuff ;)
1254 if self.show_confirmation( message, title):
1255 try:
1256 # iterate over the selection, see also on_treeDownloads_row_activated
1257 for path in paths:
1258 url = model.get_value( model.get_iter( path), 0)
1259 self.active_channel.delete_episode_by_url( url)
1260 gPodderLib().history_mark_downloaded( url)
1262 # now, clear local db cache so we can re-read it
1263 self.updateComboBox()
1264 except:
1265 log( 'Error while deleting (some) downloads.')
1267 # only delete partial files if we do not have any downloads in progress
1268 delete_partial = not services.download_status_manager.has_items()
1269 gPodderLib().clean_up_downloads( delete_partial)
1270 self.active_channel.force_update_tree_model()
1271 self.updateTreeView()
1273 def on_btnDeleteAll_clicked(self, widget, *args):
1274 self.treeAvailable.get_selection().select_all()
1275 self.on_btnDownloadedDelete_clicked( widget, args)
1276 self.treeAvailable.get_selection().unselect_all()
1279 class gPodderChannel(GladeWidget):
1280 def new(self):
1281 self.gPodderChannel.set_title( self.channel.title)
1282 self.entryTitle.set_text( self.channel.title)
1283 self.entryURL.set_text( self.channel.url)
1285 self.LabelDownloadTo.set_text( self.channel.save_dir)
1286 self.LabelWebsite.set_text( self.channel.link)
1288 self.channel.load_settings()
1289 self.cbNoSync.set_active( not self.channel.sync_to_devices)
1290 self.musicPlaylist.set_text( self.channel.device_playlist_name)
1291 self.cbMusicChannel.set_active( self.channel.is_music_channel)
1292 if self.channel.username:
1293 self.FeedUsername.set_text( self.channel.username)
1294 if self.channel.password:
1295 self.FeedPassword.set_text( self.channel.password)
1297 self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
1298 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
1300 b = gtk.TextBuffer()
1301 b.set_text( self.channel.description)
1302 self.channel_description.set_buffer( b)
1304 #Add Drag and Drop Support
1305 flags = gtk.DEST_DEFAULT_ALL
1306 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
1307 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
1308 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
1309 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
1311 def on_btnClearCover_clicked( self, widget, delete_file = True):
1312 self.imgCover.clear()
1313 if delete_file:
1314 util.delete_file( self.channel.cover_file)
1315 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1316 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1317 self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
1318 self.labelCoverStatus.show()
1320 def on_btnDownloadCover_clicked( self, widget, url = None):
1321 if url == None:
1322 url = self.channel.image
1324 if url != False:
1325 self.btnDownloadCover.set_sensitive( False)
1327 self.labelCoverStatus.show()
1328 gPodderLib().get_image_from_url( url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
1330 def cover_download_finished( self):
1331 self.labelCoverStatus.hide()
1332 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1333 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1335 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
1336 files = sel.data.strip().split('\n')
1337 if len(files) != 1:
1338 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
1339 return
1341 file = files[0]
1343 if file.startswith( 'file://') or file.startswith( 'http://'):
1344 self.on_btnClearCover_clicked( self.btnClearCover)
1345 if file.startswith( 'file://'):
1346 filename = file[len('file://'):]
1347 shutil.copyfile( filename, self.channel.cover_file)
1348 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
1349 return
1351 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
1353 def on_gPodderChannel_destroy(self, widget, *args):
1354 self.callback_closed()
1356 def on_cbMusicChannel_toggled(self, widget, *args):
1357 self.musicPlaylist.set_sensitive( self.cbMusicChannel.get_active())
1359 def on_btnOK_clicked(self, widget, *args):
1360 self.channel.sync_to_devices = not self.cbNoSync.get_active()
1361 self.channel.is_music_channel = self.cbMusicChannel.get_active()
1362 self.channel.device_playlist_name = self.musicPlaylist.get_text()
1363 self.channel.set_custom_title( self.entryTitle.get_text())
1364 self.channel.username = self.FeedUsername.get_text().strip()
1365 self.channel.password = self.FeedPassword.get_text()
1366 self.channel.save_settings()
1368 self.gPodderChannel.destroy()
1371 class gPodderProperties(GladeWidget):
1372 def new(self):
1373 if not hasattr( self, 'callback_finished'):
1374 self.callback_finished = None
1376 gl = gPodderLib()
1378 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
1379 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
1380 gl.config.connect_gtk_editable( 'player', self.openApp)
1381 gl.config.connect_gtk_editable( 'opml_url', self.opmlURL)
1382 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
1383 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
1384 gl.config.connect_gtk_togglebutton( 'download_after_update', self.downloadnew)
1385 gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
1386 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
1387 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
1388 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self. cbChannelSubfolder)
1389 gl.config.connect_gtk_spinbutton( 'max_downloads', self.spinMaxDownloads)
1390 gl.config.connect_gtk_togglebutton( 'max_downloads_enabled', self.cbMaxDownloads)
1391 gl.config.connect_gtk_spinbutton( 'limit_rate_value', self.spinLimitDownloads)
1392 gl.config.connect_gtk_togglebutton( 'limit_rate', self.cbLimitDownloads)
1393 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
1394 gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
1396 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
1397 self.radio_copy_torrents.set_active( not self.radio_gnome_bittorrent.get_active())
1399 self.iPodMountpoint.set_label( gl.config.ipod_mount)
1400 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
1401 self.chooserDownloadTo.set_filename( gl.downloaddir)
1403 if tagging_supported():
1404 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
1405 else:
1406 self.updatetags.set_sensitive( False)
1407 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
1408 self.updatetags.set_label( new_label)
1410 # device type
1411 self.comboboxDeviceType.set_active( 0)
1412 if gl.config.device_type == 'ipod':
1413 self.comboboxDeviceType.set_active( 1)
1414 elif gl.config.device_type == 'filesystem':
1415 self.comboboxDeviceType.set_active( 2)
1417 # setup cell renderers
1418 cellrenderer = gtk.CellRendererPixbuf()
1419 self.comboPlayerApp.pack_start( cellrenderer, False)
1420 self.comboPlayerApp.add_attribute( cellrenderer, 'pixbuf', 2)
1421 cellrenderer = gtk.CellRendererText()
1422 self.comboPlayerApp.pack_start( cellrenderer, True)
1423 self.comboPlayerApp.add_attribute( cellrenderer, 'markup', 0)
1425 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
1427 def update_mountpoint( self, ipod):
1428 if ipod == None or ipod.mount_point == None:
1429 self.iPodMountpoint.set_label( '')
1430 else:
1431 self.iPodMountpoint.set_label( ipod.mount_point)
1433 def set_uar( self, uar):
1434 self.comboPlayerApp.set_model( uar.get_applications_as_model())
1435 # try to activate an item
1436 index = self.find_active()
1437 self.comboPlayerApp.set_active( index)
1439 def find_active( self):
1440 model = self.comboPlayerApp.get_model()
1441 iter = model.get_iter_first()
1442 index = 0
1443 while iter != None:
1444 command = model.get_value( iter, 1)
1445 if command == self.openApp.get_text():
1446 return index
1447 iter = model.iter_next( iter)
1448 index = index + 1
1449 # return last item = custom command
1450 return index-1
1452 def set_download_dir( self, new_download_dir, event = None):
1453 gl = gPodderLib()
1454 gl.downloaddir = self.chooserDownloadTo.get_filename()
1455 if gl.downloaddir != self.chooserDownloadTo.get_filename():
1456 gobject.idle_add( self.show_message, _('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
1458 if event:
1459 event.set()
1461 def on_cbCustomSyncName_toggled( self, widget, *args):
1462 self.entryCustomSyncName.set_sensitive( widget.get_active())
1464 def on_btnCustomSyncNameHelp_clicked( self, widget):
1465 examples = [
1466 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
1467 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
1468 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
1471 info = [
1472 _('You can specify a custom format string for the file names on your MP3 player here.'),
1473 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
1474 '\n'.join( [ ' %s' % s for s in examples ])
1477 self.show_message( '\n\n'.join( info), _('Custom format strings'))
1479 def on_gPodderProperties_destroy(self, widget, *args):
1480 self.on_btnOK_clicked( widget, *args)
1482 def on_comboPlayerApp_changed(self, widget, *args):
1483 # find out which one
1484 iter = self.comboPlayerApp.get_active_iter()
1485 model = self.comboPlayerApp.get_model()
1486 command = model.get_value( iter, 1)
1487 if command == '':
1488 self.openApp.set_sensitive( True)
1489 self.openApp.show()
1490 self.labelCustomCommand.show()
1491 else:
1492 self.openApp.set_text( command)
1493 self.openApp.set_sensitive( False)
1494 self.openApp.hide()
1495 self.labelCustomCommand.hide()
1497 def on_cbMaxDownloads_toggled(self, widget, *args):
1498 self.spinMaxDownloads.set_sensitive( self.cbMaxDownloads.get_active())
1500 def on_cbLimitDownloads_toggled(self, widget, *args):
1501 self.spinLimitDownloads.set_sensitive( self.cbLimitDownloads.get_active())
1503 def on_cbEnvironmentVariables_toggled(self, widget, *args):
1504 sens = not self.cbEnvironmentVariables.get_active()
1505 self.httpProxy.set_sensitive( sens)
1506 self.ftpProxy.set_sensitive( sens)
1508 def on_comboboxDeviceType_changed(self, widget, *args):
1509 active_item = self.comboboxDeviceType.get_active()
1511 # None
1512 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
1513 self.imageSyncOptions, self. separatorSyncOptions )
1514 for widget in sync_widgets:
1515 if active_item == 0:
1516 widget.hide_all()
1517 else:
1518 widget.show_all()
1520 # iPod
1521 ipod_widgets = ( self.ipodLabel, self.btn_iPodMountpoint )
1522 for widget in ipod_widgets:
1523 if active_item == 1:
1524 widget.show_all()
1525 else:
1526 widget.hide_all()
1528 # filesystem-based MP3 player
1529 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
1530 self.cbChannelSubfolder, self.cbCustomSyncName,
1531 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
1532 for widget in fs_widgets:
1533 if active_item == 2:
1534 widget.show_all()
1535 else:
1536 widget.hide_all()
1538 def on_btn_iPodMountpoint_clicked(self, widget, *args):
1539 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
1540 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1541 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
1542 gl = gPodderLib()
1543 fs.set_filename( self.iPodMountpoint.get_label())
1544 if fs.run() == gtk.RESPONSE_OK:
1545 self.iPodMountpoint.set_label( fs.get_filename())
1546 fs.destroy()
1548 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
1549 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
1550 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1551 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
1552 gl = gPodderLib()
1553 fs.set_filename( self.filesystemMountpoint.get_label())
1554 if fs.run() == gtk.RESPONSE_OK:
1555 self.filesystemMountpoint.set_label( fs.get_filename())
1556 fs.destroy()
1558 def on_btnOK_clicked(self, widget, *args):
1559 gl = gPodderLib()
1560 gl.config.ipod_mount = self.iPodMountpoint.get_label()
1561 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
1563 if gl.downloaddir != self.chooserDownloadTo.get_filename():
1564 new_download_dir = self.chooserDownloadTo.get_filename()
1565 download_dir_size = util.calculate_size( gl.downloaddir)
1566 download_dir_size_string = gl.format_filesize( download_dir_size)
1567 event = Event()
1569 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
1570 dlg.vbox.set_spacing( 5)
1571 dlg.set_border_width( 5)
1573 label = gtk.Label()
1574 label.set_line_wrap( True)
1575 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
1576 myprogressbar = gtk.ProgressBar()
1578 # put it all together
1579 dlg.vbox.pack_start( label)
1580 dlg.vbox.pack_end( myprogressbar)
1582 # switch windows
1583 dlg.show_all()
1584 self.gPodderProperties.hide_all()
1586 # hide action area and separator line
1587 dlg.action_area.hide()
1588 dlg.set_has_separator( False)
1590 args = ( new_download_dir, event, )
1592 thread = Thread( target = self.set_download_dir, args = args)
1593 thread.start()
1595 while not event.isSet():
1596 new_download_dir_size = util.calculate_size( new_download_dir)
1597 if download_dir_size > 0:
1598 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
1599 else:
1600 fract = 0.0
1601 if fract < 0.99:
1602 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
1603 else:
1604 myprogressbar.set_text( _('Finishing... please wait.'))
1605 myprogressbar.set_fraction( fract)
1606 event.wait( 0.1)
1607 while gtk.events_pending():
1608 gtk.main_iteration( False)
1610 dlg.destroy()
1612 device_type = self.comboboxDeviceType.get_active()
1613 if device_type == 0:
1614 gl.config.device_type = 'none'
1615 elif device_type == 1:
1616 gl.config.device_type = 'ipod'
1617 elif device_type == 2:
1618 gl.config.device_type = 'filesystem'
1619 self.gPodderProperties.destroy()
1620 if self.callback_finished:
1621 self.callback_finished()
1624 class gPodderEpisode(GladeWidget):
1625 def new(self):
1626 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
1627 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
1629 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
1631 b = gtk.TextBuffer()
1632 b.set_text( strip( self.episode.description))
1633 self.episode_description.set_buffer( b)
1635 self.gPodderEpisode.set_title( self.episode.title)
1636 self.LabelDownloadLink.set_text( self.episode.url)
1637 self.LabelWebsiteLink.set_text( self.episode.link)
1638 self.labelPubDate.set_text( self.episode.pubDate)
1640 self.channel_title.set_markup( _('<i>from %s</i>') % saxutils.escape( self.channel.title))
1642 self.hide_show_widgets()
1643 services.download_status_manager.request_progress_detail( self.episode.url)
1645 def on_btnCancel_clicked( self, widget):
1646 services.download_status_manager.cancel_by_url( self.episode.url)
1648 def on_gPodderEpisode_destroy( self, widget):
1649 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
1650 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
1652 def on_download_status_changed( self):
1653 self.hide_show_widgets()
1655 def on_download_status_progress( self, url, progress, speed):
1656 if url == self.episode.url:
1657 self.progress_bar.set_fraction( 1.0*progress/100.0)
1658 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
1660 def hide_show_widgets( self):
1661 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
1662 if is_downloading:
1663 self.progress_bar.show_all()
1664 self.btnCancel.show_all()
1665 self.btnPlay.hide_all()
1666 self.btnSaveFile.hide_all()
1667 self.btnDownload.hide_all()
1668 else:
1669 self.progress_bar.hide_all()
1670 self.btnCancel.hide_all()
1671 if os.path.exists( self.episode.local_filename()):
1672 self.btnPlay.show_all()
1673 self.btnSaveFile.show_all()
1674 self.btnDownload.hide_all()
1675 else:
1676 self.btnPlay.hide_all()
1677 self.btnSaveFile.hide_all()
1678 self.btnDownload.show_all()
1680 def on_btnCloseWindow_clicked(self, widget, *args):
1681 self.gPodderEpisode.destroy()
1683 def on_btnDownload_clicked(self, widget, *args):
1684 if self.download_callback:
1685 self.download_callback()
1687 def on_btnPlay_clicked(self, widget, *args):
1688 if self.play_callback:
1689 self.play_callback()
1691 self.gPodderEpisode.destroy()
1693 def on_btnSaveFile_clicked(self, widget, *args):
1694 self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
1697 class gPodderSync(GladeWidget):
1698 def new(self):
1699 self.pos_overall = 0
1700 self.max_overall = 1
1701 self.pos_episode = 0
1702 self.max_episode = 1
1703 self.cancel_button.set_sensitive( False)
1704 self.sync = None
1705 self.default_title = self.gPodderSync.get_title()
1706 self.default_header = self.label_header.get_text()
1707 self.default_body = self.label_text.get_text()
1709 self.imageSync.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
1711 def set_sync_object( self, sync):
1712 self.sync = sync
1713 if self.sync.can_cancel:
1714 self.cancel_button.set_sensitive( True)
1716 def set_progress( self, pos, max, is_overall = False, is_sub_episode = False):
1717 pos = min(pos, max)
1718 if is_sub_episode:
1719 fraction_episode = 1.0*(self.pos_episode+1.0*pos/max)/self.max_episode
1720 self.pbEpisode.set_fraction( fraction_episode)
1721 self.pbSync.set_fraction( 1.0*(self.pos_overall+fraction_episode)/self.max_overall)
1722 return
1724 if is_overall:
1725 progressbar = self.pbSync
1726 self.pos_overall = pos
1727 self.max_overall = max
1728 progressbar.set_fraction( 1.0*pos/max)
1729 else:
1730 progressbar = self.pbEpisode
1731 self.pos_episode = pos
1732 self.max_episode = max
1733 progressbar.set_fraction( 1.0*pos/max)
1734 self.pbSync.set_fraction( 1.0*(self.pos_overall+1.0*pos/max)/self.max_overall)
1736 percent = _('%d of %d done') % ( pos, max )
1737 progressbar.set_text( percent)
1739 def set_status( self, episode = None, channel = None, progressbar = None, title = None, header = None, body = None):
1740 if episode != None:
1741 self.labelEpisode.set_markup( '<i>%s</i>' % saxutils.escape( episode))
1743 if channel != None:
1744 self.labelChannel.set_markup( '<i>%s</i>' % saxutils.escape( channel))
1746 if progressbar != None:
1747 self.pbSync.set_text( progressbar)
1749 if title != None:
1750 self.gPodderSync.set_title( title)
1751 else:
1752 self.gPodderSync.set_title( self.default_title)
1754 if header != None:
1755 self.label_header.set_markup( '<b><big>%s</big></b>' % saxutils.escape( header))
1756 else:
1757 self.label_header.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.default_header))
1759 if body != None:
1760 self.label_text.set_text( body)
1761 else:
1762 self.label_text.set_text( self.default_body)
1765 def close( self, success = True, access_error = False, cleaned = False, error_messages = []):
1766 if self.sync:
1767 self.sync.cancelled = True
1768 self.cancel_button.set_label( gtk.STOCK_CLOSE)
1769 self.cancel_button.set_use_stock( True)
1770 self.cancel_button.set_sensitive( True)
1771 self.gPodderSync.set_resizable( True)
1772 self.pbSync.hide_all()
1773 self.pbEpisode.hide_all()
1774 self.labelChannel.hide_all()
1775 self.labelEpisode.hide_all()
1776 self.gPodderSync.set_resizable( False)
1777 if success and not cleaned:
1778 title = _('Synchronization finished')
1779 header = _('Copied Podcasts')
1780 body = _('The selected episodes have been copied to your device. You can now unplug the device.')
1781 elif access_error:
1782 title = _('Synchronization error')
1783 header = _('Cannot access device')
1784 body = _('Make sure your device is connected to your computer and mounted. Please also make sure you have set the correct path to your device in the preferences dialog.')
1785 elif cleaned:
1786 title = _('Device cleaned')
1787 header = _('Podcasts removed')
1788 body = _('Synchronized Podcasts have been removed from your device.')
1789 elif len(error_messages):
1790 title = _('Synchronization error')
1791 header = _('An error has occurred')
1792 body = '\n'.join( error_messages)
1793 else:
1794 title = _('Synchronization aborted')
1795 header = _('Aborted')
1796 body = _('The synchronization progress has been interrupted by the user. Please retry synchronization at a later time.')
1797 self.gPodderSync.set_title( title)
1798 self.label_header.set_markup( '<big><b>%s</b></big>' % saxutils.escape( header))
1799 self.label_text.set_text( body)
1801 def on_gPodderSync_destroy(self, widget, *args):
1802 pass
1804 def on_cancel_button_clicked(self, widget, *args):
1805 if self.sync:
1806 if self.sync.cancelled:
1807 self.gPodderSync.destroy()
1808 else:
1809 self.sync.cancelled = True
1810 self.cancel_button.set_sensitive( False)
1811 else:
1812 self.gPodderSync.destroy()
1815 class gPodderOpmlLister(GladeWidget):
1816 def new(self):
1817 # initiate channels list
1818 self.channels = []
1819 self.callback_for_channel = None
1820 self.callback_finished = None
1822 togglecell = gtk.CellRendererToggle()
1823 togglecell.set_property( 'activatable', True)
1824 togglecell.connect( 'toggled', self.callback_edited)
1825 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
1827 titlecell = gtk.CellRendererText()
1828 titlecolumn = gtk.TreeViewColumn( _('Channel'), titlecell, markup=1)
1830 for itemcolumn in ( togglecolumn, titlecolumn ):
1831 self.treeviewChannelChooser.append_column( itemcolumn)
1833 def callback_edited( self, cell, path):
1834 model = self.treeviewChannelChooser.get_model()
1836 url = model[path][2]
1838 model[path][0] = not model[path][0]
1839 if model[path][0]:
1840 self.channels.append( url)
1841 else:
1842 self.channels.remove( url)
1844 self.btnOK.set_sensitive( bool(len(self.channels)))
1846 def thread_func( self):
1847 url = self.entryURL.get_text()
1848 importer = opml.Importer( url)
1849 model = importer.get_model()
1850 gobject.idle_add( self.treeviewChannelChooser.set_model, model)
1851 gobject.idle_add( self.labelStatus.set_label, '')
1852 gobject.idle_add( self.btnDownloadOpml.set_sensitive, True)
1853 gobject.idle_add( self.entryURL.set_sensitive, True)
1854 gobject.idle_add( self.treeviewChannelChooser.set_sensitive, True)
1855 self.channels = []
1857 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
1858 if callback_for_channel:
1859 self.callback_for_channel = callback_for_channel
1860 if callback_finished:
1861 self.callback_finished = callback_finished
1862 self.labelStatus.set_label( _('Downloading, please wait...'))
1863 self.entryURL.set_text( url)
1864 self.btnDownloadOpml.set_sensitive( False)
1865 self.entryURL.set_sensitive( False)
1866 self.btnOK.set_sensitive( False)
1867 self.treeviewChannelChooser.set_sensitive( False)
1868 Thread( target = self.thread_func).start()
1870 def on_gPodderOpmlLister_destroy(self, widget, *args):
1871 pass
1873 def on_btnDownloadOpml_clicked(self, widget, *args):
1874 self.get_channels_from_url( self.entryURL.get_text())
1876 def on_btnOK_clicked(self, widget, *args):
1877 self.gPodderOpmlLister.destroy()
1879 # add channels that have been selected
1880 for url in self.channels:
1881 if self.callback_for_channel:
1882 self.callback_for_channel( url)
1884 if self.callback_finished:
1885 self.callback_finished()
1887 def on_btnCancel_clicked(self, widget, *args):
1888 self.gPodderOpmlLister.destroy()
1891 class gPodderEpisodeSelector( GladeWidget):
1892 """Episode selection dialog
1894 Optional keyword arguments that modify the behaviour of this dialog:
1896 - callback: Function that takes 1 parameter which is a list of
1897 the selected episodes (or empty list when none selected)
1898 - episodes: List of episodes that are presented for selection
1899 - selected: (optional) List of boolean variables that define the
1900 default checked state for the given episodes
1901 - selected_default: (optional) The default boolean value for the
1902 checked state if no other value is set
1903 (default is False)
1904 - columns: List of (name,caption) pairs for the columns, the name
1905 is the attribute name of the episode to be read from
1906 each episode object and the caption attribute is the
1907 text that appear as column caption
1908 (default is [('title','Episode'),])
1909 - title: (optional) The title of the window + heading
1910 - instructions: (optional) A one-line text describing what the
1911 user should select / what the selection is for
1912 - stock_ok_button: (optional) Will replace the "OK" button with
1913 another GTK+ stock item to be used for the
1914 affirmative button of the dialog (e.g. can
1915 be gtk.STOCK_DELETE when the episodes to be
1916 selected will be deleted after closing the
1917 dialog)
1918 - selection_buttons: (optional) A dictionary with labels as
1919 keys and callbacks as values; for each
1920 key a button will be generated, and when
1921 the button is clicked, the callback will
1922 be called for each episode and the return
1923 value of the callback (True or False) will
1924 be the new selected state of the episode
1925 - size_attribute: (optional) The name of an attribute of the
1926 supplied episode objects that can be used to
1927 calculate the size of an episode; set this to
1928 None if no total size calculation should be
1929 done (in cases where total size is useless)
1930 (default is 'length')
1933 COLUMN_TOGGLE = 0
1934 COLUMN_ADDITIONAL = 1
1936 def new( self):
1937 if not hasattr( self, 'callback'):
1938 self.callback = None
1940 if not hasattr( self, 'episodes'):
1941 self.episodes = []
1943 if not hasattr( self, 'size_attribute'):
1944 self.size_attribute = 'length'
1946 if not hasattr( self, 'selection_buttons'):
1947 self.selection_buttons = {}
1949 if not hasattr( self, 'selected_default'):
1950 self.selected_default = False
1952 if not hasattr( self, 'selected'):
1953 self.selected = [self.selected_default]*len(self.episodes)
1955 if len(self.selected) < len(self.episodes):
1956 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
1958 if not hasattr( self, 'columns'):
1959 self.columns = ( ('title', _('Episode')), )
1961 if hasattr( self, 'title'):
1962 self.gPodderEpisodeSelector.set_title( self.title)
1963 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
1965 if hasattr( self, 'instructions'):
1966 self.labelInstructions.set_text( self.instructions)
1967 self.labelInstructions.show_all()
1969 if hasattr( self, 'stock_ok_button'):
1970 self.btnOK.set_label( self.stock_ok_button)
1971 self.btnOK.set_use_stock( True)
1973 toggle_cell = gtk.CellRendererToggle()
1974 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
1976 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
1978 next_column = self.COLUMN_ADDITIONAL
1979 for name, caption in self.columns:
1980 renderer = gtk.CellRendererText()
1981 if next_column > self.COLUMN_ADDITIONAL:
1982 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
1983 column = gtk.TreeViewColumn( caption, renderer, text=next_column)
1984 column.set_resizable( True)
1985 column.set_expand( True)
1986 self.treeviewEpisodes.append_column( column)
1987 next_column += 1
1989 column_types = [ gobject.TYPE_BOOLEAN ] + [ gobject.TYPE_STRING ] * len(self.columns)
1990 self.model = gtk.ListStore( *column_types)
1992 for index, episode in enumerate( self.episodes):
1993 row = [ self.selected[index] ]
1994 for name, caption in self.columns:
1995 row.append( getattr( episode, name))
1996 self.model.append( row)
1998 for label in self.selection_buttons:
1999 button = gtk.Button( label)
2000 button.connect( 'clicked', self.custom_selection_button_clicked)
2001 self.hboxButtons.pack_start( button, expand = False)
2002 button.show_all()
2004 self.treeviewEpisodes.set_rules_hint( True)
2005 self.treeviewEpisodes.set_model( self.model)
2006 self.treeviewEpisodes.columns_autosize()
2007 self.calculate_total_size()
2009 def calculate_total_size( self):
2010 if self.size_attribute is not None:
2011 total_size = 0
2012 for index, row in enumerate( self.model):
2013 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2014 try:
2015 total_size += int(getattr( self.episodes[index], self.size_attribute))
2016 except:
2017 log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
2019 if total_size > 0:
2020 self.labelTotalSize.set_text( _('Total size: %s') % util.format_filesize( total_size))
2021 else:
2022 self.labelTotalSize.set_text( '')
2023 self.labelTotalSize.show_all()
2024 else:
2025 self.labelTotalSize.hide_all()
2027 def toggle_cell_handler( self, cell, path):
2028 model = self.treeviewEpisodes.get_model()
2029 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
2031 if self.size_attribute is not None:
2032 self.calculate_total_size()
2034 def custom_selection_button_clicked( self, button):
2035 label = button.get_label()
2036 callback = self.selection_buttons[label]
2038 for index, row in enumerate( self.model):
2039 new_value = callback( self.episodes[index])
2040 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
2042 self.calculate_total_size()
2044 def on_btnCheckAll_clicked( self, widget):
2045 for row in self.model:
2046 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
2048 self.calculate_total_size()
2050 def on_btnCheckNone_clicked( self, widget):
2051 for row in self.model:
2052 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
2054 self.calculate_total_size()
2056 def get_selected_episodes( self):
2057 selected_episodes = []
2059 for index, row in enumerate( self.model):
2060 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2061 selected_episodes.append( self.episodes[index])
2063 return selected_episodes
2065 def on_btnOK_clicked( self, widget):
2066 self.gPodderEpisodeSelector.destroy()
2067 if self.callback is not None:
2068 self.callback( self.get_selected_episodes())
2070 def on_btnCancel_clicked( self, widget):
2071 self.gPodderEpisodeSelector.destroy()
2074 def main():
2075 gobject.threads_init()
2076 gtk.window_set_default_icon_name( 'gpodder')
2078 gPodder().run()