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/>.
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
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
__
77 SimpleGladeApp
.SimpleGladeApp
.__init
__( self
, path
, root
, domain
, **kwargs
)
80 GladeWidget
.gpodder_main_window
= self
.gPodder
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)
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
)
102 dlg
.set_title( title
)
103 dlg
.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title
, message
))
105 dlg
.set_markup( '<span weight="bold" size="larger">%s</span>' % ( message
))
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
)
114 dlg
.set_title( title
)
115 dlg
.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title
, message
))
117 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % message
)
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
)
153 shutil
.copyfile( src_filename
, dst_filename
)
155 log( 'Error copying file.', sender
= self
, traceback
= True)
161 class gPodder(GladeWidget
):
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
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
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
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
))
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
)
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
)
332 menu
.popup( None, None, None, event
.button
, event
.time
)
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
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
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
371 first_url
= model
.get_value( model
.get_iter( paths
[0]), 0)
375 ( can_play
, can_download
, can_transfer
, can_cancel
) = self
.play_or_download()
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
))
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
))
394 menu
.append( gtk
.SeparatorMenuItem())
396 episode_title
= _('%d selected episodes') % len(paths
)
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
))
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
)
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
))
414 menu
.append( gtk
.SeparatorMenuItem())
415 is_downloaded
= gPodderLib().history_is_downloaded( first_url
)
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))
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))
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
))
434 menu
.append( gtk
.SeparatorMenuItem())
435 is_played
= gPodderLib().history_is_played( first_url
)
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))
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))
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
))
454 menu
.popup( None, None, None, event
.button
, event
.time
)
458 def download_progress_updated( self
, count
, percentage
):
459 title
= [ self
.default_title
]
462 title
.append( _('downloading one file'))
464 title
.append( _('downloading %d files') % count
)
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
)
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):
483 # columns, as defined in libpodcasts' get model method
484 # 1 = episode title, 7 = description
487 for column
in columns
:
488 value
= model
.get_value( iter, column
).lower()
489 if value
.find( key
) != -1:
494 def play_or_download( self
):
495 if self
.wNotebook
.get_current_page() > 0:
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()
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
):
511 if services
.download_status_manager
.is_download_in_progress( url
):
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()
533 self
.labelDownloads
.set_text( _('Downloads (%d)') % count
)
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()
546 selected
= model
.get_path( iter)
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
)
558 self
.treeChannels
.get_selection().select_path( selected
)
560 log( 'Cannot set selection on treeChannels', sender
= self
)
561 self
.on_treeChannels_cursor_changed( self
.treeChannels
)
563 def updateTreeView( self
):
567 self
.treeAvailable
.set_model( self
.active_channel
.tree_model
)
568 self
.treeAvailable
.columns_autosize()
569 self
.play_or_download()
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
):
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
)
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
,))
591 log( 'Adding new channel: %s', result
)
593 channel
= podcastChannel
.get_by_url( url
= result
, force_update
= True)
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()
615 self
.on_btnDownloadNewer_clicked( None)
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
)
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):
628 sync
.close( success
= False, access_error
= True)
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
)
640 sync
.set_progress_overall( i
, len(downloaded_channels
))
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
):
653 gobject
.idle_add( self
.show_message
, message
, title
)
654 sync
.close( success
= False, access_error
= True)
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
)
665 title
= _('Please wait...')
667 label
.set_markup( '<i>%s</i>' % title
)
669 progressbar
.set_text( _('%d of %d channels updated') % ( position
, 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
)
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
)
733 self
.updateComboBox()
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:
743 current_channel
= self
.active_channel
744 current_podcast
= current_channel
.find_episode( url
)
745 filename
= current_podcast
.local_filename()
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
)
752 if current_podcast
.file_type() != 'torrent':
753 self
.playback_episode( current_channel
, current_podcast
)
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
)
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()
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
):
781 save_channels( self
.channels
)
783 services
.download_status_manager
.cancel_all()
790 def for_each_selected_episode_url( self
, callback
):
791 ( model
, paths
) = self
.treeAvailable
.get_selection().get_selected_rows()
793 url
= model
.get_value( model
.get_iter( path
), 0)
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:
805 if len(episodes
) == 1:
806 message
= _('Do you really want to delete this episode?')
808 message
= _('Do you really want to delete %d episodes?') % len(episodes
)
810 if confirm
and self
.show_confirmation( message
, _('Delete episodes')) == False:
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
):
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.')
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):
849 callback
= lambda url
: gPodderLib().history_mark_downloaded( url
, not gPodderLib().history_is_downloaded( url
))
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):
857 callback
= lambda url
: gPodderLib().history_mark_played( url
, not gPodderLib().history_is_played( url
))
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
):
865 self
.update_feed_cache()
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
):
880 ('title', _('Episode')),
881 ('channel_prop', _('Channel')),
882 ('filesize_prop', _('Size')),
883 ('pubdate_prop', _('Released')),
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
)
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
):
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
)
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
)
917 if gl
.config
.device_type
in [ 'ipod', 'filesystem' ]:
920 if gl
.config
.device_type
== 'filesystem':
921 sync_class
= gPodder_FSSync
922 elif gl
.config
.device_type
== 'ipod':
923 sync_class
= gPodder_iPodSync
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
]
933 thread_args
.append( args
[0])
934 thread
= Thread( target
= self
.sync_to_ipod_proc
, args
= thread_args
)
937 def on_cleanup_ipod_activate(self
, widget
, *args
):
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
)
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
)
951 if gl
.config
.device_type
in [ 'ipod', 'filesystem' ]:
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
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
, ))
974 def update_item_device( self
):
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())
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
)
1005 gPodderChannel( channel
= self
.active_channel
, callback_closed
= self
.updateComboBox
)
1007 def on_itemRemoveChannel_activate(self
, widget
, *args
):
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)
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
)
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'))
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())
1070 dlg
.set_logo( gtk
.gdk
.pixbuf_new_from_file_at_size( scalable_dir
, 200, 200))
1076 def on_wNotebook_switch_page(self
, widget
, *args
):
1079 self
.play_or_download()
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()
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
):
1121 selection
= self
.treeAvailable
.get_selection()
1122 selection_tuple
= selection
.get_selected_rows()
1123 transfer_files
= False
1126 if selection
.count_selected_rows() > 1:
1127 widget_to_send
= None
1128 show_message_dialog
= False
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)
1141 episodes
.append( self
.active_channel
.find_episode( url
))
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
)
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
)
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
] )
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
, )
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
):
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
):
1195 if self
.wNotebook
.get_current_page() > 0:
1196 # Use the download list treeview + model
1197 ( tree
, column
) = ( self
.treeDownloads
, 3 )
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()
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
)
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.")
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:
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.')
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.")
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
):
1256 # iterate over the selection, see also on_treeDownloads_row_activated
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()
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
):
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()
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):
1322 url
= self
.channel
.image
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')
1338 self
.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
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)
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
):
1373 if not hasattr( self
, 'callback_finished'):
1374 self
.callback_finished
= None
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
)
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
)
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( '')
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()
1444 command
= model
.get_value( iter, 1)
1445 if command
== self
.openApp
.get_text():
1447 iter = model
.iter_next( iter)
1449 # return last item = custom command
1452 def set_download_dir( self
, new_download_dir
, event
= None):
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'))
1461 def on_cbCustomSyncName_toggled( self
, widget
, *args
):
1462 self
.entryCustomSyncName
.set_sensitive( widget
.get_active())
1464 def on_btnCustomSyncNameHelp_clicked( self
, widget
):
1466 '<i>{episode.title}</i> -> <b>Interview with RMS</b>',
1467 '<i>{episode.basename}</i> -> <b>70908-interview-rms</b>',
1468 '<i>{episode.published}</i> -> <b>20070908</b>'
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)
1488 self
.openApp
.set_sensitive( True)
1490 self
.labelCustomCommand
.show()
1492 self
.openApp
.set_text( command
)
1493 self
.openApp
.set_sensitive( False)
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()
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:
1521 ipod_widgets
= ( self
.ipodLabel
, self
.btn_iPodMountpoint
)
1522 for widget
in ipod_widgets
:
1523 if active_item
== 1:
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:
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
)
1543 fs
.set_filename( self
.iPodMountpoint
.get_label())
1544 if fs
.run() == gtk
.RESPONSE_OK
:
1545 self
.iPodMountpoint
.set_label( fs
.get_filename())
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
)
1553 fs
.set_filename( self
.filesystemMountpoint
.get_label())
1554 if fs
.run() == gtk
.RESPONSE_OK
:
1555 self
.filesystemMountpoint
.set_label( fs
.get_filename())
1558 def on_btnOK_clicked(self
, widget
, *args
):
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
)
1569 dlg
= gtk
.Dialog( _('Moving downloads folder'), self
.gPodderProperties
)
1570 dlg
.vbox
.set_spacing( 5)
1571 dlg
.set_border_width( 5)
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
)
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
)
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
)
1602 myprogressbar
.set_text( _('%s of %s') % ( gl
.format_filesize( new_download_dir_size
), download_dir_size_string
, ))
1604 myprogressbar
.set_text( _('Finishing... please wait.'))
1605 myprogressbar
.set_fraction( fract
)
1607 while gtk
.events_pending():
1608 gtk
.main_iteration( False)
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
):
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
)
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()
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()
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
):
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)
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
):
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):
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
)
1725 progressbar
= self
.pbSync
1726 self
.pos_overall
= pos
1727 self
.max_overall
= max
1728 progressbar
.set_fraction( 1.0*pos
/max)
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):
1741 self
.labelEpisode
.set_markup( '<i>%s</i>' % saxutils
.escape( episode
))
1744 self
.labelChannel
.set_markup( '<i>%s</i>' % saxutils
.escape( channel
))
1746 if progressbar
!= None:
1747 self
.pbSync
.set_text( progressbar
)
1750 self
.gPodderSync
.set_title( title
)
1752 self
.gPodderSync
.set_title( self
.default_title
)
1755 self
.label_header
.set_markup( '<b><big>%s</big></b>' % saxutils
.escape( header
))
1757 self
.label_header
.set_markup( '<b><big>%s</big></b>' % saxutils
.escape( self
.default_header
))
1760 self
.label_text
.set_text( body
)
1762 self
.label_text
.set_text( self
.default_body
)
1765 def close( self
, success
= True, access_error
= False, cleaned
= False, error_messages
= []):
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.')
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.')
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
)
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
):
1804 def on_cancel_button_clicked(self
, widget
, *args
):
1806 if self
.sync
.cancelled
:
1807 self
.gPodderSync
.destroy()
1809 self
.sync
.cancelled
= True
1810 self
.cancel_button
.set_sensitive( False)
1812 self
.gPodderSync
.destroy()
1815 class gPodderOpmlLister(GladeWidget
):
1817 # initiate channels list
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]
1840 self
.channels
.append( url
)
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)
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
):
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
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
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')
1934 COLUMN_ADDITIONAL
= 1
1937 if not hasattr( self
, 'callback'):
1938 self
.callback
= None
1940 if not hasattr( 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
)
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)
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:
2012 for index
, row
in enumerate( self
.model
):
2013 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
2015 total_size
+= int(getattr( self
.episodes
[index
], self
.size_attribute
))
2017 log( 'Cannot get size for %s', self
.episodes
[index
].title
, sender
= self
)
2020 self
.labelTotalSize
.set_text( _('Total size: %s') % util
.format_filesize( total_size
))
2022 self
.labelTotalSize
.set_text( '')
2023 self
.labelTotalSize
.show_all()
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()
2075 gobject
.threads_init()
2076 gtk
.window_set_default_icon_name( 'gpodder')